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.
Files changed (150) hide show
  1. package/README.md +7 -0
  2. package/core/activate.js +27 -0
  3. package/core/adopt.js +71 -0
  4. package/core/auth-core.js +73 -0
  5. package/core/auth-fetch.js +154 -0
  6. package/core/component-mount.js +110 -0
  7. package/core/defaults.js +48 -0
  8. package/core/define.js +15 -0
  9. package/core/display-target.js +166 -0
  10. package/core/edit-placements.js +28 -0
  11. package/core/editor-self.js +127 -0
  12. package/core/editor.js +162 -0
  13. package/core/events.js +27 -0
  14. package/core/extension-points.js +189 -0
  15. package/core/form-utils.js +210 -0
  16. package/core/from-query.js +138 -0
  17. package/core/from-rdf.js +52 -0
  18. package/core/here.js +33 -0
  19. package/core/include-core.js +73 -0
  20. package/core/inrupt-global.js +18 -0
  21. package/core/menu-consumer.js +41 -0
  22. package/core/menu-rdf.js +154 -0
  23. package/core/pod-ops.js +392 -0
  24. package/core/pod-registry.js +82 -0
  25. package/core/popup-proxy.js +255 -0
  26. package/core/rdf-core.js +280 -0
  27. package/core/rdf-render.js +136 -0
  28. package/core/rdf-utils.js +411 -0
  29. package/core/rdf.js +154 -0
  30. package/core/services.js +106 -0
  31. package/core/shape-to-form.js +741 -0
  32. package/core/sparql-safety.js +20 -0
  33. package/core/utils.js +196 -0
  34. package/dist/importmap-cdn.json +49 -0
  35. package/dist/importmap-local.json +49 -0
  36. package/dist/sol-loader.manifest.json +140 -0
  37. package/dist/vendor/@comunica-query-sparql.js +137851 -0
  38. package/dist/vendor/@inrupt-solid-client-authn-browser.js +7503 -0
  39. package/dist/vendor/dompurify.js +1476 -0
  40. package/dist/vendor/ical.js.js +9739 -0
  41. package/dist/vendor/marked.js +85 -0
  42. package/dist/vendor/n3.js +14670 -0
  43. package/dist/vendor/rdf-validate-shacl.js +6970 -0
  44. package/dist/vendor/rdflib.js +35172 -0
  45. package/dist/vendor/solid-logic.js +6819 -0
  46. package/dist/vendor/solid-ui.js +21945 -0
  47. package/node/sol-form.js +133 -0
  48. package/node/sol-include.js +55 -0
  49. package/node/sol-login.js +632 -0
  50. package/node/sol-menu.js +639 -0
  51. package/node/sol-query.js +116 -0
  52. package/package.json +133 -0
  53. package/web/menu-from-rdf.js +23 -0
  54. package/web/scripts/prefs.js +25 -0
  55. package/web/sol-accordion.js +114 -0
  56. package/web/sol-basic.js +50 -0
  57. package/web/sol-breadcrumb.js +131 -0
  58. package/web/sol-button.js +244 -0
  59. package/web/sol-calendar.js +465 -0
  60. package/web/sol-default.js +118 -0
  61. package/web/sol-dropdown-button.js +222 -0
  62. package/web/sol-feed.js +1336 -0
  63. package/web/sol-form.js +949 -0
  64. package/web/sol-full.js +43 -0
  65. package/web/sol-gallery.js +303 -0
  66. package/web/sol-include.js +246 -0
  67. package/web/sol-live-edit.js +415 -0
  68. package/web/sol-login.js +856 -0
  69. package/web/sol-menu.js +593 -0
  70. package/web/sol-modal.js +377 -0
  71. package/web/sol-pod-extras.js +17 -0
  72. package/web/sol-pod-ops.js +680 -0
  73. package/web/sol-pod.js +1039 -0
  74. package/web/sol-query.js +546 -0
  75. package/web/sol-rolodex.js +95 -0
  76. package/web/sol-search.js +402 -0
  77. package/web/sol-settings.js +199 -0
  78. package/web/sol-solidos.js +93 -0
  79. package/web/sol-tabs.js +445 -0
  80. package/web/sol-time.js +194 -0
  81. package/web/sol-tree-edit.js +492 -0
  82. package/web/sol-wac.js +456 -0
  83. package/web/sol-weather.js +337 -0
  84. package/web/sol-window.js +142 -0
  85. package/web/styles/buttons-css.js +108 -0
  86. package/web/styles/help.css +242 -0
  87. package/web/styles/root.css +112 -0
  88. package/web/styles/sol-accordion-css.js +97 -0
  89. package/web/styles/sol-calendar-css.js +154 -0
  90. package/web/styles/sol-feed-css.js +475 -0
  91. package/web/styles/sol-form-css.js +471 -0
  92. package/web/styles/sol-gallery-css.js +181 -0
  93. package/web/styles/sol-include-css.js +95 -0
  94. package/web/styles/sol-live-edit-css.js +84 -0
  95. package/web/styles/sol-live-edit.css +101 -0
  96. package/web/styles/sol-login-css.js +116 -0
  97. package/web/styles/sol-menu-css.js +145 -0
  98. package/web/styles/sol-modal-css.js +134 -0
  99. package/web/styles/sol-pod-css.js +187 -0
  100. package/web/styles/sol-pod-modal-css.js +203 -0
  101. package/web/styles/sol-query-css.js +140 -0
  102. package/web/styles/sol-query-help.css +267 -0
  103. package/web/styles/sol-query-one-pager.css +67 -0
  104. package/web/styles/sol-search-css.js +157 -0
  105. package/web/styles/sol-solidos-css.js +7 -0
  106. package/web/styles/sol-tabs-css.js +114 -0
  107. package/web/styles/sol-time-css.js +30 -0
  108. package/web/styles/sol-wac-css.js +73 -0
  109. package/web/styles/sol-weather-css.js +59 -0
  110. package/web/styles/solid-logo.svg +9 -0
  111. package/web/styles/view-accordion-css.js +66 -0
  112. package/web/styles/view-anchorlist-css.js +22 -0
  113. package/web/styles/view-autocomplete-css.js +59 -0
  114. package/web/styles/view-rolodex-css.js +102 -0
  115. package/web/styles/view-select-css.js +21 -0
  116. package/web/utils/calendar-fetch.js +388 -0
  117. package/web/utils/code-mirror-editor.js +82 -0
  118. package/web/utils/commons-fetch.js +108 -0
  119. package/web/utils/feed-edit.js +159 -0
  120. package/web/utils/feed-edit.smoke.mjs +74 -0
  121. package/web/utils/feed-fetch.js +573 -0
  122. package/web/utils/live-edit-help/csv.js +64 -0
  123. package/web/utils/live-edit-help/graphviz.js +41 -0
  124. package/web/utils/live-edit-help/jsonld.js +55 -0
  125. package/web/utils/live-edit-help/markdown.js +52 -0
  126. package/web/utils/live-edit-help/mermaid.js +48 -0
  127. package/web/utils/live-edit-help/turtle.js +85 -0
  128. package/web/utils/rdf-config.js +125 -0
  129. package/web/utils/renderers/csv.js +124 -0
  130. package/web/utils/renderers/d3-force.js +82 -0
  131. package/web/utils/renderers/graphviz.js +13 -0
  132. package/web/utils/renderers/html.js +10 -0
  133. package/web/utils/renderers/jsonld.js +63 -0
  134. package/web/utils/renderers/markdown.js +19 -0
  135. package/web/utils/renderers/mermaid.js +54 -0
  136. package/web/utils/renderers/turtle.js +51 -0
  137. package/web/utils/sol-query-triple-patterns.js +151 -0
  138. package/web/utils/sol-query-ui.js +250 -0
  139. package/web/utils/sol-query-views.js +32 -0
  140. package/web/views/_helpers.js +34 -0
  141. package/web/views/accordion.js +133 -0
  142. package/web/views/anchorlist.js +59 -0
  143. package/web/views/auto-complete.js +183 -0
  144. package/web/views/dl.js +38 -0
  145. package/web/views/list.js +19 -0
  146. package/web/views/menu.js +56 -0
  147. package/web/views/rolodex.js +126 -0
  148. package/web/views/select.js +79 -0
  149. package/web/views/table.js +73 -0
  150. 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();