@cube-nl/dmp-sdk 1.0.0-rc.4 → 1.1.0-rc.2
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 +372 -9
- package/dist/app.d.ts +41 -4
- package/dist/app.js +252 -14
- package/dist/browser.js +14 -1
- package/dist/client.d.ts +2 -0
- package/dist/client.js +5 -1
- package/dist/cube-sdk.js +186 -2
- package/dist/cube-ui.manifest.json +971 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/preview.d.ts +11 -0
- package/dist/preview.js +60 -7
- package/dist/size.json +6 -0
- package/dist/trial.d.ts +32 -0
- package/dist/trial.js +55 -0
- package/dist/ui/aggregate-block.d.ts +73 -0
- package/dist/ui/aggregate-block.js +139 -0
- package/dist/ui/attributes.d.ts +5 -0
- package/dist/ui/attributes.js +72 -0
- package/dist/ui/author-error.d.ts +3 -0
- package/dist/ui/author-error.js +3 -0
- package/dist/ui/block.d.ts +69 -0
- package/dist/ui/block.js +238 -0
- package/dist/ui/blocks/app-shell.d.ts +4 -0
- package/dist/ui/blocks/app-shell.js +167 -0
- package/dist/ui/blocks/board.d.ts +8 -0
- package/dist/ui/blocks/board.js +333 -0
- package/dist/ui/blocks/calc.d.ts +9 -0
- package/dist/ui/blocks/calc.js +263 -0
- package/dist/ui/blocks/chart.d.ts +7 -0
- package/dist/ui/blocks/chart.js +166 -0
- package/dist/ui/blocks/detail.d.ts +8 -0
- package/dist/ui/blocks/detail.js +270 -0
- package/dist/ui/blocks/field.d.ts +8 -0
- package/dist/ui/blocks/field.js +16 -0
- package/dist/ui/blocks/form.d.ts +8 -0
- package/dist/ui/blocks/form.js +153 -0
- package/dist/ui/blocks/gap-list.d.ts +4 -0
- package/dist/ui/blocks/gap-list.js +36 -0
- package/dist/ui/blocks/kpi.d.ts +4 -0
- package/dist/ui/blocks/kpi.js +75 -0
- package/dist/ui/blocks/period-picker.d.ts +4 -0
- package/dist/ui/blocks/period-picker.js +155 -0
- package/dist/ui/blocks/state.d.ts +4 -0
- package/dist/ui/blocks/state.js +135 -0
- package/dist/ui/blocks/table.d.ts +8 -0
- package/dist/ui/blocks/table.js +666 -0
- package/dist/ui/blocks/top.d.ts +4 -0
- package/dist/ui/blocks/top.js +67 -0
- package/dist/ui/chart-svg.d.ts +28 -0
- package/dist/ui/chart-svg.js +111 -0
- package/dist/ui/csv.d.ts +9 -0
- package/dist/ui/csv.js +55 -0
- package/dist/ui/define.d.ts +24 -0
- package/dist/ui/define.js +107 -0
- package/dist/ui/dom.d.ts +12 -0
- package/dist/ui/dom.js +58 -0
- package/dist/ui/fields.d.ts +39 -0
- package/dist/ui/fields.js +73 -0
- package/dist/ui/format.d.ts +44 -0
- package/dist/ui/format.js +215 -0
- package/dist/ui/inputs.d.ts +35 -0
- package/dist/ui/inputs.js +161 -0
- package/dist/ui/manifest.d.ts +40 -0
- package/dist/ui/manifest.js +880 -0
- package/dist/ui/period.d.ts +18 -0
- package/dist/ui/period.js +52 -0
- package/dist/ui/query.d.ts +26 -0
- package/dist/ui/query.js +99 -0
- package/dist/ui/schema.d.ts +23 -0
- package/dist/ui/schema.js +21 -0
- package/dist/ui/styles.d.ts +8 -0
- package/dist/ui/styles.js +51 -0
- package/dist/ui/texts.d.ts +146 -0
- package/dist/ui/texts.js +138 -0
- package/dist/ui/theme.d.ts +58 -0
- package/dist/ui/theme.js +311 -0
- package/dist/ui/trial-banner.d.ts +25 -0
- package/dist/ui/trial-banner.js +180 -0
- package/dist/ui/write.d.ts +20 -0
- package/dist/ui/write.js +54 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/wireframe.d.ts +37 -0
- package/dist/wireframe.js +352 -0
- package/fonts/OFL-albert-sans.txt +93 -0
- package/fonts/OFL-ibm-plex-sans.txt +93 -0
- package/fonts/OFL-inter.txt +93 -0
- package/fonts/OFL-nunito-sans.txt +93 -0
- package/fonts/OFL-source-sans-3.txt +93 -0
- package/fonts/albert-sans.woff2 +0 -0
- package/fonts/ibm-plex-sans.woff2 +0 -0
- package/fonts/inter.woff2 +0 -0
- package/fonts/nunito-sans.woff2 +0 -0
- package/fonts/source-sans-3.woff2 +0 -0
- package/package.json +12 -4
package/dist/ui/block.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { CubeApp } from '../app.js';
|
|
2
|
+
import { isDmpError } from '../errors.js';
|
|
3
|
+
import { BLOCK_ATTRIBUTES } from './attributes.js';
|
|
4
|
+
import { AuthorError } from './author-error.js';
|
|
5
|
+
import { clear, h } from './dom.js';
|
|
6
|
+
import { BASE_CSS, sheet } from './styles.js';
|
|
7
|
+
import { NL } from './texts.js';
|
|
8
|
+
/** Thrown by a block's load to show exactly this state. */
|
|
9
|
+
export class BlockStateError extends Error {
|
|
10
|
+
state;
|
|
11
|
+
constructor(state) {
|
|
12
|
+
super(state.detail ?? state.kind);
|
|
13
|
+
this.state = state;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* What a failure looks like to the person on the page. Only an AuthorError carries its message: the blocks throw
|
|
18
|
+
* those for a wrong attribute, in Dutch. A server's text, a network failure or a client's English programmer error
|
|
19
|
+
* never does.
|
|
20
|
+
*/
|
|
21
|
+
export function stateFor(error) {
|
|
22
|
+
if (error instanceof BlockStateError)
|
|
23
|
+
return error.state;
|
|
24
|
+
if (isDmpError(error)) {
|
|
25
|
+
if (error.status === 401)
|
|
26
|
+
return { kind: 'signed-out', detail: null };
|
|
27
|
+
if (error.status === 403)
|
|
28
|
+
return { kind: 'forbidden', detail: null };
|
|
29
|
+
}
|
|
30
|
+
if (error instanceof AuthorError)
|
|
31
|
+
return { kind: 'error', detail: error.message };
|
|
32
|
+
return { kind: 'error', detail: null };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolves once the page's own scripts have run. A block must not take the page session before the app had its
|
|
36
|
+
* chance to call `CubeApp.connect()` with its own options, or the block would start a second, default one.
|
|
37
|
+
*/
|
|
38
|
+
export function pageReady(doc) {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
if (doc.readyState === 'loading') {
|
|
41
|
+
doc.addEventListener('DOMContentLoaded', () => resolve(), { once: true });
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
(doc.defaultView?.setTimeout ?? setTimeout)(resolve, 0);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** The header mark of an invented field: `Voorbeeld` on screen, the whole of exampleMissing read out. */
|
|
49
|
+
export function exampleBadge(doc) {
|
|
50
|
+
return h(doc, 'span', { class: 'badge example' }, NL.example, h(doc, 'span', { class: 'sr-only' }, NL.exampleMissing.slice(NL.example.length)));
|
|
51
|
+
}
|
|
52
|
+
function isAbort(error) {
|
|
53
|
+
return error !== null && typeof error === 'object' && error.name === 'AbortError';
|
|
54
|
+
}
|
|
55
|
+
export function blockBase(scope, options = {}) {
|
|
56
|
+
const getSession = options.session ?? (() => CubeApp.ready());
|
|
57
|
+
class CubeBlock extends scope.HTMLElement {
|
|
58
|
+
static tag = '';
|
|
59
|
+
static css = '';
|
|
60
|
+
static needsSession = true;
|
|
61
|
+
static reloadsOnOwnChange = true;
|
|
62
|
+
static get observedAttributes() {
|
|
63
|
+
return [...(BLOCK_ATTRIBUTES[this.tag] ?? [])];
|
|
64
|
+
}
|
|
65
|
+
body;
|
|
66
|
+
current = Promise.resolve();
|
|
67
|
+
scheduled = false;
|
|
68
|
+
controller = null;
|
|
69
|
+
/** Outside the body, so a block that redraws its body (a board move, an edit) keeps it. */
|
|
70
|
+
badge = null;
|
|
71
|
+
onSessionChange = () => this.schedule();
|
|
72
|
+
onDataChange = (event) => {
|
|
73
|
+
if (event.composedPath()[0] === this && !this.constructor.reloadsOnOwnChange)
|
|
74
|
+
return;
|
|
75
|
+
const resource = event.detail?.resource;
|
|
76
|
+
const own = this.getAttribute('resource')?.trim() ?? '';
|
|
77
|
+
if (typeof resource === 'string' && own !== '' && resource.trim() === own)
|
|
78
|
+
this.schedule();
|
|
79
|
+
};
|
|
80
|
+
constructor() {
|
|
81
|
+
super();
|
|
82
|
+
const ctor = this.constructor;
|
|
83
|
+
const root = this.attachShadow({ mode: 'open' });
|
|
84
|
+
root.adoptedStyleSheets = ctor.css === ''
|
|
85
|
+
? [sheet(scope, BASE_CSS)]
|
|
86
|
+
: [sheet(scope, BASE_CSS), sheet(scope, ctor.css)];
|
|
87
|
+
this.body = h(scope.document, 'div', { class: 'cube-body', part: 'body' });
|
|
88
|
+
root.appendChild(this.body);
|
|
89
|
+
// Bubbles up from the cube-state in this block's own shadow tree; an outer block must not reload too.
|
|
90
|
+
root.addEventListener('cube-retry', (event) => {
|
|
91
|
+
event.stopPropagation();
|
|
92
|
+
this.schedule();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
get updateComplete() {
|
|
96
|
+
return (async () => {
|
|
97
|
+
let seen;
|
|
98
|
+
do {
|
|
99
|
+
seen = this.current;
|
|
100
|
+
await seen;
|
|
101
|
+
} while (seen !== this.current);
|
|
102
|
+
})();
|
|
103
|
+
}
|
|
104
|
+
connectedCallback() {
|
|
105
|
+
this.ownerDocument.addEventListener('cube-session-change', this.onSessionChange);
|
|
106
|
+
this.ownerDocument.addEventListener('cube-data-change', this.onDataChange);
|
|
107
|
+
this.schedule();
|
|
108
|
+
}
|
|
109
|
+
disconnectedCallback() {
|
|
110
|
+
this.ownerDocument.removeEventListener('cube-session-change', this.onSessionChange);
|
|
111
|
+
this.ownerDocument.removeEventListener('cube-data-change', this.onDataChange);
|
|
112
|
+
this.controller?.abort();
|
|
113
|
+
this.controller = null;
|
|
114
|
+
this.removeAttribute('aria-busy');
|
|
115
|
+
}
|
|
116
|
+
attributeChangedCallback(_name, oldValue, newValue) {
|
|
117
|
+
if (oldValue !== newValue)
|
|
118
|
+
this.schedule();
|
|
119
|
+
}
|
|
120
|
+
refresh() {
|
|
121
|
+
if (!this.isConnected)
|
|
122
|
+
return this.current;
|
|
123
|
+
this.controller?.abort();
|
|
124
|
+
const controller = new AbortController();
|
|
125
|
+
this.controller = controller;
|
|
126
|
+
const run = this.run(controller.signal);
|
|
127
|
+
this.current = run;
|
|
128
|
+
return run;
|
|
129
|
+
}
|
|
130
|
+
/** Several changes in one tick (attributes set one by one, say) make one load. */
|
|
131
|
+
schedule() {
|
|
132
|
+
if (this.scheduled || !this.isConnected)
|
|
133
|
+
return;
|
|
134
|
+
this.scheduled = true;
|
|
135
|
+
this.current = new Promise((resolve) => {
|
|
136
|
+
queueMicrotask(() => {
|
|
137
|
+
this.scheduled = false;
|
|
138
|
+
resolve(this.isConnected ? this.refresh() : undefined);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async run(signal) {
|
|
143
|
+
const ctor = this.constructor;
|
|
144
|
+
this.showState('loading');
|
|
145
|
+
try {
|
|
146
|
+
await pageReady(this.ownerDocument);
|
|
147
|
+
if (signal.aborted)
|
|
148
|
+
return;
|
|
149
|
+
const session = ctor.needsSession ? await this.session() : null;
|
|
150
|
+
if (signal.aborted)
|
|
151
|
+
return;
|
|
152
|
+
await this.load(signal, session);
|
|
153
|
+
if (!signal.aborted)
|
|
154
|
+
this.mark(session);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (signal.aborted || isAbort(error))
|
|
158
|
+
return;
|
|
159
|
+
this.badge?.remove();
|
|
160
|
+
this.showState(stateFor(error));
|
|
161
|
+
// The page shows only the generic text; the builder finds the real reason in the console.
|
|
162
|
+
if (error instanceof TypeError && !(error instanceof AuthorError)) {
|
|
163
|
+
console.warn(`${this.localName}:`, error);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
// An aborted run leaves the flag to the newer run (or to disconnect).
|
|
168
|
+
if (!signal.aborted)
|
|
169
|
+
this.removeAttribute('aria-busy');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
session() {
|
|
173
|
+
return getSession();
|
|
174
|
+
}
|
|
175
|
+
shownFields() {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
// Optional calls: a session made by hand (a test, an older embedder) may not know about wireframes.
|
|
179
|
+
exampleField(session, resource, field) {
|
|
180
|
+
return session.isExample?.(resource, field) === true && !session.isExample(resource);
|
|
181
|
+
}
|
|
182
|
+
notSaved(session) {
|
|
183
|
+
return session.simulatesWrites === true
|
|
184
|
+
? h(this.ownerDocument, 'p', { class: 'note not-saved' }, NL.exampleNotSaved)
|
|
185
|
+
: null;
|
|
186
|
+
}
|
|
187
|
+
mark(session) {
|
|
188
|
+
const resource = this.getAttribute('resource')?.trim() ?? '';
|
|
189
|
+
const marked = session?.isExample !== undefined && resource !== ''
|
|
190
|
+
&& (session.isExample(resource) || this.shownFields().some((f) => session.isExample(resource, f)));
|
|
191
|
+
if (!marked) {
|
|
192
|
+
this.badge?.remove();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
this.badge ??= h(this.ownerDocument, 'p', { class: 'badge example', role: 'note' }, NL.exampleMissing);
|
|
196
|
+
this.shadowRoot?.insertBefore(this.badge, this.body);
|
|
197
|
+
}
|
|
198
|
+
showState(state, detail = null) {
|
|
199
|
+
const { kind, detail: message, retry } = typeof state === 'string' ? { kind: state, detail } : state;
|
|
200
|
+
// Busy until the run ends; a block that draws its own loading (the shell's frame) is never busy.
|
|
201
|
+
if (kind === 'loading')
|
|
202
|
+
this.setAttribute('aria-busy', 'true');
|
|
203
|
+
clear(this.body);
|
|
204
|
+
this.body.appendChild(h(this.ownerDocument, 'cube-state', { kind, message, retry: retry === false ? 'false' : null }));
|
|
205
|
+
}
|
|
206
|
+
attr(name) {
|
|
207
|
+
return this.getAttribute(name);
|
|
208
|
+
}
|
|
209
|
+
flag(name) {
|
|
210
|
+
return this.hasAttribute(name) && this.getAttribute(name) !== 'false';
|
|
211
|
+
}
|
|
212
|
+
num(name, fallback) {
|
|
213
|
+
const raw = this.getAttribute(name);
|
|
214
|
+
if (raw === null || raw.trim() === '')
|
|
215
|
+
return fallback;
|
|
216
|
+
const value = Number(raw);
|
|
217
|
+
if (!Number.isFinite(value))
|
|
218
|
+
throw new AuthorError(NL.invalid.number(this.localName, name));
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
221
|
+
json(name) {
|
|
222
|
+
const raw = this.getAttribute(name);
|
|
223
|
+
if (raw === null || raw.trim() === '')
|
|
224
|
+
return null;
|
|
225
|
+
try {
|
|
226
|
+
return JSON.parse(raw);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
throw new AuthorError(NL.invalid.json(this.localName, name));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
emit(name, detail = null) {
|
|
233
|
+
const View = this.ownerDocument.defaultView?.CustomEvent ?? CustomEvent;
|
|
234
|
+
return this.dispatchEvent(new View(name, { detail, bubbles: true, composed: true }));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return CubeBlock;
|
|
238
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { BlockClass } from '../block.js';
|
|
2
|
+
import type { BlockScope } from '../define.js';
|
|
3
|
+
/** `<cube-app-shell>`: header, page navigation and the content slot. The content shows whatever the session does. */
|
|
4
|
+
export declare function appShellBlock(Base: BlockClass, scope: BlockScope): BlockClass;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { clear, h } from '../dom.js';
|
|
2
|
+
import { NL } from '../texts.js';
|
|
3
|
+
import { currentTheme, onTheme } from '../theme.js';
|
|
4
|
+
const CSS = `
|
|
5
|
+
:host{background:var(--cube-color-bg)}
|
|
6
|
+
header{display:flex;flex-wrap:wrap;align-items:center;gap:.75rem 1rem;padding:.75rem 1.5rem;
|
|
7
|
+
border-bottom:1px solid var(--cube-color-border);background:var(--cube-color-surface)}
|
|
8
|
+
.logo{display:block;height:2rem;width:auto;max-width:10rem;object-fit:contain}
|
|
9
|
+
h1{flex:1 1 auto;min-width:0;margin:0;font-size:1.25rem;line-height:1.3;overflow-wrap:anywhere}
|
|
10
|
+
.user{display:flex;flex-wrap:wrap;align-items:center;gap:.75rem;margin-left:auto}
|
|
11
|
+
nav{border-bottom:1px solid var(--cube-color-border);padding:0 1.5rem}
|
|
12
|
+
nav ul{display:flex;gap:.25rem;margin:0;padding:0;list-style:none;position:relative;overflow-x:auto}
|
|
13
|
+
nav a{display:block;padding:.75rem 1rem;white-space:nowrap;text-decoration:none;color:var(--cube-color-text-muted);
|
|
14
|
+
border-bottom:3px solid transparent}
|
|
15
|
+
nav a:hover{color:var(--cube-color-primary-text)}
|
|
16
|
+
nav a[aria-current=page]{font-weight:600;color:var(--cube-color-primary-text);
|
|
17
|
+
border-bottom-color:var(--cube-color-primary)}
|
|
18
|
+
main{padding:1.5rem}
|
|
19
|
+
::slotted(cube-page:not([hidden])){display:flex;flex-direction:column;gap:1.5rem}
|
|
20
|
+
@media (max-width:640px){header{padding:.75rem 1rem}nav{padding:0}main{padding:1rem}}
|
|
21
|
+
`;
|
|
22
|
+
const PREFIX = '#/';
|
|
23
|
+
/** `<cube-app-shell>`: header, page navigation and the content slot. The content shows whatever the session does. */
|
|
24
|
+
export function appShellBlock(Base, scope) {
|
|
25
|
+
class CubeAppShell extends Base {
|
|
26
|
+
static tag = 'cube-app-shell';
|
|
27
|
+
static css = CSS;
|
|
28
|
+
header;
|
|
29
|
+
heading;
|
|
30
|
+
user;
|
|
31
|
+
nav;
|
|
32
|
+
list;
|
|
33
|
+
main;
|
|
34
|
+
/** The page names and labels the navigation was built from, to rebuild it only when they change. */
|
|
35
|
+
built = '';
|
|
36
|
+
stopTheme = null;
|
|
37
|
+
onHash = () => this.updatePages();
|
|
38
|
+
constructor() {
|
|
39
|
+
super();
|
|
40
|
+
const doc = scope.document;
|
|
41
|
+
this.heading = h(doc, 'h1');
|
|
42
|
+
this.user = h(doc, 'div', { class: 'user' });
|
|
43
|
+
this.header = h(doc, 'header', {}, this.heading, this.user);
|
|
44
|
+
this.list = h(doc, 'ul');
|
|
45
|
+
this.nav = h(doc, 'nav', { 'aria-label': NL.pages }, this.list);
|
|
46
|
+
const slot = h(doc, 'slot', { onslotchange: () => this.updatePages() });
|
|
47
|
+
this.main = h(doc, 'main', {}, slot);
|
|
48
|
+
this.body.append(this.header, this.main);
|
|
49
|
+
this.renderHeader();
|
|
50
|
+
}
|
|
51
|
+
connectedCallback() {
|
|
52
|
+
super.connectedCallback();
|
|
53
|
+
this.ownerDocument.defaultView?.addEventListener('hashchange', this.onHash);
|
|
54
|
+
this.stopTheme?.();
|
|
55
|
+
this.stopTheme = onTheme(() => this.renderHeader());
|
|
56
|
+
this.renderHeader();
|
|
57
|
+
this.updatePages();
|
|
58
|
+
}
|
|
59
|
+
disconnectedCallback() {
|
|
60
|
+
super.disconnectedCallback();
|
|
61
|
+
this.ownerDocument.defaultView?.removeEventListener('hashchange', this.onHash);
|
|
62
|
+
this.stopTheme?.();
|
|
63
|
+
this.stopTheme = null;
|
|
64
|
+
}
|
|
65
|
+
async load(_signal, session) {
|
|
66
|
+
this.renderHeader();
|
|
67
|
+
this.updatePages();
|
|
68
|
+
if (session !== null)
|
|
69
|
+
this.renderUser(session);
|
|
70
|
+
}
|
|
71
|
+
// The frame stays; only the user area reflects a state. Signed out offers sign-in, anything else stays empty.
|
|
72
|
+
showState(state) {
|
|
73
|
+
const kind = typeof state === 'string' ? state : state.kind;
|
|
74
|
+
clear(this.user);
|
|
75
|
+
if (kind === 'signed-out')
|
|
76
|
+
this.user.appendChild(this.signInButton());
|
|
77
|
+
}
|
|
78
|
+
renderHeader() {
|
|
79
|
+
const name = this.attr('name') ?? '';
|
|
80
|
+
const src = this.attr('logo') ?? currentTheme().logo;
|
|
81
|
+
const current = this.header.querySelector('.logo');
|
|
82
|
+
this.heading.textContent = name;
|
|
83
|
+
if (src === null || src === '') {
|
|
84
|
+
current?.remove();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const logo = h(scope.document, 'img', { class: 'logo', src, alt: this.attr('logo-alt') ?? name });
|
|
88
|
+
if (current === null)
|
|
89
|
+
this.header.prepend(logo);
|
|
90
|
+
else
|
|
91
|
+
current.replaceWith(logo);
|
|
92
|
+
}
|
|
93
|
+
renderUser(session) {
|
|
94
|
+
const doc = scope.document;
|
|
95
|
+
const user = session.user();
|
|
96
|
+
clear(this.user);
|
|
97
|
+
if (user === null) {
|
|
98
|
+
this.user.appendChild(this.signInButton());
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.user.append(h(doc, 'span', {}, user.name !== null && user.name !== '' ? user.name : NL.signedIn), h(doc, 'button', { type: 'button', class: 'secondary', onclick: () => void this.change((s) => s.signOut()) }, NL.signOut));
|
|
102
|
+
}
|
|
103
|
+
signInButton() {
|
|
104
|
+
return h(scope.document, 'button', { type: 'button', onclick: () => void this.change((s) => s.signIn()) }, NL.signIn);
|
|
105
|
+
}
|
|
106
|
+
async change(action) {
|
|
107
|
+
try {
|
|
108
|
+
await action(await this.session());
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Nothing changed: show the user area as the session has it, so the button is there to try again.
|
|
112
|
+
void this.refresh();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const View = this.ownerDocument.defaultView?.CustomEvent ?? CustomEvent;
|
|
116
|
+
this.ownerDocument.dispatchEvent(new View('cube-session-change'));
|
|
117
|
+
}
|
|
118
|
+
pages() {
|
|
119
|
+
return Array.from(this.children).filter((child) => child.localName === 'cube-page');
|
|
120
|
+
}
|
|
121
|
+
/** Shows the page the address names (default the first) and marks it in the navigation. */
|
|
122
|
+
updatePages() {
|
|
123
|
+
const pages = this.pages();
|
|
124
|
+
const location = scope.location ?? this.ownerDocument.defaultView?.location;
|
|
125
|
+
const hash = location?.hash ?? '';
|
|
126
|
+
let wanted = '';
|
|
127
|
+
if (hash.startsWith(PREFIX)) {
|
|
128
|
+
try {
|
|
129
|
+
wanted = decodeURIComponent(hash.slice(PREFIX.length).split('?')[0] ?? '');
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
wanted = '';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const active = pages.find((page) => page.getAttribute('name') === wanted) ?? pages[0];
|
|
136
|
+
for (const page of pages)
|
|
137
|
+
page.hidden = page !== active;
|
|
138
|
+
// Not hidden but absent: a single page has no navigation at all, also not for a screen reader.
|
|
139
|
+
if (pages.length < 2) {
|
|
140
|
+
this.nav.remove();
|
|
141
|
+
clear(this.list);
|
|
142
|
+
this.built = '';
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (this.nav.parentNode !== this.body)
|
|
146
|
+
this.body.insertBefore(this.nav, this.main);
|
|
147
|
+
const signature = JSON.stringify(pages.map((page) => [page.getAttribute('name'), page.getAttribute('label')]));
|
|
148
|
+
if (signature !== this.built) {
|
|
149
|
+
clear(this.list);
|
|
150
|
+
for (const page of pages) {
|
|
151
|
+
const name = page.getAttribute('name') ?? '';
|
|
152
|
+
const href = PREFIX + encodeURIComponent(name);
|
|
153
|
+
const link = h(scope.document, 'a', { href }, page.getAttribute('label') ?? name);
|
|
154
|
+
this.list.appendChild(h(scope.document, 'li', {}, link));
|
|
155
|
+
}
|
|
156
|
+
this.built = signature;
|
|
157
|
+
}
|
|
158
|
+
Array.from(this.list.querySelectorAll('a')).forEach((link, index) => {
|
|
159
|
+
if (pages[index] === active)
|
|
160
|
+
link.setAttribute('aria-current', 'page');
|
|
161
|
+
else
|
|
162
|
+
link.removeAttribute('aria-current');
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return CubeAppShell;
|
|
167
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type BlockClass } from '../block.js';
|
|
2
|
+
import type { BlockScope } from '../define.js';
|
|
3
|
+
/**
|
|
4
|
+
* `<cube-board>`: the rows of a resource as cards in a column per value of a status field. A card moves by drag
|
|
5
|
+
* and drop or by a button per other column ("Verplaats naar …"): at once on screen, then saved; a refusal puts it
|
|
6
|
+
* back.
|
|
7
|
+
*/
|
|
8
|
+
export declare function boardBlock(Base: BlockClass, scope: BlockScope): BlockClass;
|