@mosaicajs/components 0.3.0 → 0.4.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 +2 -0
- package/dist/aggregate.d.ts +4 -10
- package/dist/aggregate.js +7 -13
- package/dist/artifact-card.d.ts +21 -7
- package/dist/artifact-card.js +136 -79
- package/dist/base.d.ts +44 -5
- package/dist/base.js +54 -5
- package/dist/cluster-filter.d.ts +29 -25
- package/dist/cluster-filter.js +123 -106
- package/dist/colour-editor.d.ts +15 -4
- package/dist/colour-editor.js +107 -34
- package/dist/colour-picker.d.ts +12 -5
- package/dist/colour-picker.js +119 -32
- package/dist/display.js +77 -9
- package/dist/events.d.ts +21 -7
- package/dist/explorer.d.ts +116 -37
- package/dist/explorer.js +476 -210
- package/dist/field-card.d.ts +304 -48
- package/dist/field-card.js +1364 -244
- package/dist/filter-panel.d.ts +54 -10
- package/dist/filter-panel.js +242 -42
- package/dist/filter.d.ts +22 -4
- package/dist/filter.js +113 -34
- package/dist/hierarchy.d.ts +68 -45
- package/dist/hierarchy.js +320 -243
- package/dist/item-card.d.ts +47 -16
- package/dist/item-card.js +241 -108
- package/dist/key-picker.d.ts +2 -1
- package/dist/key-picker.js +4 -3
- package/dist/layer-picker.d.ts +4 -2
- package/dist/layer-picker.js +16 -7
- package/dist/lineage.d.ts +132 -0
- package/dist/lineage.js +307 -0
- package/dist/map.d.ts +135 -24
- package/dist/map.js +387 -117
- package/dist/mosaica-components.js +1837 -833
- package/dist/mosaica-components.js.sri +1 -1
- package/dist/mosaica-components.json +2 -2
- package/dist/parts.d.ts +4 -4
- package/dist/parts.js +18 -4
- package/dist/paths.d.ts +34 -12
- package/dist/paths.js +84 -46
- package/dist/selection.d.ts +22 -3
- package/dist/selection.js +125 -59
- package/dist/states.d.ts +72 -0
- package/dist/states.js +135 -26
- package/dist/status.d.ts +63 -5
- package/dist/status.js +215 -36
- package/dist/tokens.d.ts +8 -4
- package/dist/tokens.js +144 -14
- package/dist/view-picker.js +5 -0
- package/package.json +4 -3
package/dist/item-card.js
CHANGED
|
@@ -35,7 +35,7 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
|
|
|
35
35
|
import { css, html, nothing } from 'lit';
|
|
36
36
|
import { property, state } from 'lit/decorators.js';
|
|
37
37
|
import { GRID32 } from '@mosaicajs/client';
|
|
38
|
-
import { MosaicaElement, columnCaption, emit, idString, keyTitle, timestampText } from './base.js';
|
|
38
|
+
import { BOOL_WORDS, MosaicaElement, columnCaption, emit, idString, keyTitle, timestampText } from './base.js';
|
|
39
39
|
import { attachContextRoot, defineOnce } from './define.js';
|
|
40
40
|
import { icon } from './icons.js';
|
|
41
41
|
import { reasonOf, refusedState, renderState, stateOf } from './states.js';
|
|
@@ -47,19 +47,25 @@ import { chrome, tokens } from './tokens.js';
|
|
|
47
47
|
*
|
|
48
48
|
* The headline is the `title-field` field, or the item's `mosaica_id` when that field is unset or
|
|
49
49
|
* empty. To render a field your own way, as a link for example, put an element in its
|
|
50
|
-
* `field-<name>` slot.
|
|
50
|
+
* `field-<name>` slot. The card is headed "Item" for a screen reader, with the headline a heading
|
|
51
|
+
* under it.
|
|
52
|
+
*
|
|
53
|
+
* A value over 60 characters is kept to six lines, with a Show more button under it where that
|
|
54
|
+
* cuts it short. A `compact` card also keeps its headline to three lines and its subtitle to two,
|
|
55
|
+
* and its Show all button shows every field and all of each.
|
|
51
56
|
*
|
|
52
57
|
* A field named in `image-fields` whose value is an `http:` or `https:` address, or a path
|
|
53
58
|
* resolved against the page, is drawn as the image at that address, across the card's width. The viewer's browser loads it from the host
|
|
54
|
-
* that serves it, without a referrer.
|
|
55
|
-
* not
|
|
59
|
+
* that serves it, without a referrer. Its alternative text is the item's `title-field` value, or
|
|
60
|
+
* the field's name where the item has none. A value that is not such an address, and an image that
|
|
61
|
+
* does not load, are shown as text.
|
|
56
62
|
*
|
|
57
63
|
* The Open button appears only when you set `open-url` or `open`. `open-url` is a link template
|
|
58
64
|
* such as `https://www.geonames.org/{geonameid}`, in which each `{name}` is replaced by the item's
|
|
59
65
|
* value for that field. Because the values are URL-encoded, a template cannot open an address that
|
|
60
66
|
* a field already holds, so set `open` to a function for that. An item gets no Open button when it
|
|
61
|
-
* has no value for a field the template names. A function in `open`
|
|
62
|
-
* `mosaica_id` and fields, and it takes precedence over `open-url`.
|
|
67
|
+
* has no value for a field the template names. The link opens in a new tab. A function in `open`
|
|
68
|
+
* is called with the item's `mosaica_id` and fields, and it takes precedence over `open-url`.
|
|
63
69
|
*
|
|
64
70
|
* @summary A card showing an item's fields, with an optional Open button.
|
|
65
71
|
* @tagname mosaica-item-card
|
|
@@ -74,23 +80,26 @@ import { chrome, tokens } from './tokens.js';
|
|
|
74
80
|
* @csspart title - The header row: the headline or the state, and the close button.
|
|
75
81
|
* @csspart close - The close button.
|
|
76
82
|
* @csspart state - The state line, with `data-state`: `shown`, `empty` (no item, or a click that
|
|
77
|
-
* found nothing), `refused` or `detached`.
|
|
83
|
+
* found nothing), `refused` or `detached`. It is inside a `role="status"` region.
|
|
78
84
|
* @csspart refusal - The words "Item unavailable", with `data-code` set to the refusal's code if
|
|
79
85
|
* the server refused the record. For a broken pick, the details are in the map's `lastPick`.
|
|
80
86
|
* @csspart reason - The server's reason for refusing the record, under "Item unavailable".
|
|
81
|
-
* @csspart headline - The headline, with `data-name` set to the field it shows.
|
|
87
|
+
* @csspart headline - The headline, an `<h3>`, with `data-name` set to the field it shows.
|
|
82
88
|
* @csspart subtitle - The `subtitle-field` value under the headline, when `compact` is set.
|
|
83
89
|
* @csspart show-all - The "Show all N fields" button, when `compact` is set, with `aria-expanded`.
|
|
84
|
-
*
|
|
85
|
-
*
|
|
90
|
+
* It reads "Show all" where every field is shown and only text is cut short.
|
|
91
|
+
* @csspart view-chip - One view the item is in, with `data-view`, and `aria-current` and a bar
|
|
92
|
+
* under the current view.
|
|
86
93
|
* @csspart field - One field, with `data-name`, and `data-prose` on a value over 60 characters.
|
|
87
|
-
* @csspart label - A field's name, and the heading above the view chips.
|
|
88
|
-
* @csspart value - A field's value
|
|
94
|
+
* @csspart label - A field's name, a `<dt>`, and the heading above the view chips.
|
|
95
|
+
* @csspart value - A field's value, a `<dd>`.
|
|
96
|
+
* @csspart more - The Show more button under a long value that is cut short, with `aria-expanded`.
|
|
89
97
|
* @csspart image - A field's value drawn as an image, under `image-fields`.
|
|
90
98
|
* @csspart scoped - The group-scoped values, grouped by key.
|
|
91
99
|
* @csspart key - One key's heading among the group-scoped values, with `data-key`.
|
|
92
100
|
* @csspart open - The Open button.
|
|
93
|
-
* @csspart copy - The Copy id button, which copies the `mosaica_id` to the clipboard
|
|
101
|
+
* @csspart copy - The Copy id button, which copies the `mosaica_id` to the clipboard and says to a
|
|
102
|
+
* screen reader whether it did.
|
|
94
103
|
*/
|
|
95
104
|
let MosaicaItemCard = (() => {
|
|
96
105
|
let _classSuper = MosaicaElement;
|
|
@@ -130,6 +139,15 @@ let MosaicaItemCard = (() => {
|
|
|
130
139
|
let _expanded_decorators;
|
|
131
140
|
let _expanded_initializers = [];
|
|
132
141
|
let _expanded_extraInitializers = [];
|
|
142
|
+
let _opened_decorators;
|
|
143
|
+
let _opened_initializers = [];
|
|
144
|
+
let _opened_extraInitializers = [];
|
|
145
|
+
let _clipped_decorators;
|
|
146
|
+
let _clipped_initializers = [];
|
|
147
|
+
let _clipped_extraInitializers = [];
|
|
148
|
+
let _copied_decorators;
|
|
149
|
+
let _copied_initializers = [];
|
|
150
|
+
let _copied_extraInitializers = [];
|
|
133
151
|
let _broken_decorators;
|
|
134
152
|
let _broken_initializers = [];
|
|
135
153
|
let _broken_extraInitializers = [];
|
|
@@ -148,6 +166,9 @@ let MosaicaItemCard = (() => {
|
|
|
148
166
|
_openUrl_decorators = [property({ attribute: 'open-url' })];
|
|
149
167
|
_open_decorators = [property({ attribute: false })];
|
|
150
168
|
_expanded_decorators = [state()];
|
|
169
|
+
_opened_decorators = [state()];
|
|
170
|
+
_clipped_decorators = [state()];
|
|
171
|
+
_copied_decorators = [state()];
|
|
151
172
|
_broken_decorators = [state()];
|
|
152
173
|
__esDecorate(this, null, _item_decorators, { kind: "accessor", name: "item", static: false, private: false, access: { has: obj => "item" in obj, get: obj => obj.item, set: (obj, value) => { obj.item = value; } }, metadata: _metadata }, _item_initializers, _item_extraInitializers);
|
|
153
174
|
__esDecorate(this, null, _refusal_decorators, { kind: "accessor", name: "refusal", static: false, private: false, access: { has: obj => "refusal" in obj, get: obj => obj.refusal, set: (obj, value) => { obj.refusal = value; } }, metadata: _metadata }, _refusal_initializers, _refusal_extraInitializers);
|
|
@@ -161,6 +182,9 @@ let MosaicaItemCard = (() => {
|
|
|
161
182
|
__esDecorate(this, null, _openUrl_decorators, { kind: "accessor", name: "openUrl", static: false, private: false, access: { has: obj => "openUrl" in obj, get: obj => obj.openUrl, set: (obj, value) => { obj.openUrl = value; } }, metadata: _metadata }, _openUrl_initializers, _openUrl_extraInitializers);
|
|
162
183
|
__esDecorate(this, null, _open_decorators, { kind: "accessor", name: "open", static: false, private: false, access: { has: obj => "open" in obj, get: obj => obj.open, set: (obj, value) => { obj.open = value; } }, metadata: _metadata }, _open_initializers, _open_extraInitializers);
|
|
163
184
|
__esDecorate(this, null, _expanded_decorators, { kind: "accessor", name: "expanded", static: false, private: false, access: { has: obj => "expanded" in obj, get: obj => obj.expanded, set: (obj, value) => { obj.expanded = value; } }, metadata: _metadata }, _expanded_initializers, _expanded_extraInitializers);
|
|
185
|
+
__esDecorate(this, null, _opened_decorators, { kind: "accessor", name: "opened", static: false, private: false, access: { has: obj => "opened" in obj, get: obj => obj.opened, set: (obj, value) => { obj.opened = value; } }, metadata: _metadata }, _opened_initializers, _opened_extraInitializers);
|
|
186
|
+
__esDecorate(this, null, _clipped_decorators, { kind: "accessor", name: "clipped", static: false, private: false, access: { has: obj => "clipped" in obj, get: obj => obj.clipped, set: (obj, value) => { obj.clipped = value; } }, metadata: _metadata }, _clipped_initializers, _clipped_extraInitializers);
|
|
187
|
+
__esDecorate(this, null, _copied_decorators, { kind: "accessor", name: "copied", static: false, private: false, access: { has: obj => "copied" in obj, get: obj => obj.copied, set: (obj, value) => { obj.copied = value; } }, metadata: _metadata }, _copied_initializers, _copied_extraInitializers);
|
|
164
188
|
__esDecorate(this, null, _broken_decorators, { kind: "accessor", name: "broken", static: false, private: false, access: { has: obj => "broken" in obj, get: obj => obj.broken, set: (obj, value) => { obj.broken = value; } }, metadata: _metadata }, _broken_initializers, _broken_extraInitializers);
|
|
165
189
|
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
166
190
|
}
|
|
@@ -175,7 +199,7 @@ let MosaicaItemCard = (() => {
|
|
|
175
199
|
display: flex;
|
|
176
200
|
align-items: flex-start;
|
|
177
201
|
justify-content: space-between;
|
|
178
|
-
gap:
|
|
202
|
+
gap: 0;
|
|
179
203
|
margin: 0 0 10px;
|
|
180
204
|
font-size: 13px;
|
|
181
205
|
font-weight: 400;
|
|
@@ -183,6 +207,11 @@ let MosaicaItemCard = (() => {
|
|
|
183
207
|
text-transform: none;
|
|
184
208
|
color: var(--_mosaica-ink);
|
|
185
209
|
}
|
|
210
|
+
/* The state's region is in the row whether or not it says anything, and takes room only when it does. */
|
|
211
|
+
.said {
|
|
212
|
+
min-width: 0;
|
|
213
|
+
}
|
|
214
|
+
.said.alone,
|
|
186
215
|
.head [part='headline'],
|
|
187
216
|
.head .headline {
|
|
188
217
|
flex: 1 1 auto;
|
|
@@ -193,6 +222,13 @@ let MosaicaItemCard = (() => {
|
|
|
193
222
|
flex-direction: column;
|
|
194
223
|
gap: 3px;
|
|
195
224
|
}
|
|
225
|
+
h3[part='headline'] {
|
|
226
|
+
display: block;
|
|
227
|
+
margin: 0;
|
|
228
|
+
letter-spacing: 0;
|
|
229
|
+
text-transform: none;
|
|
230
|
+
color: var(--_mosaica-ink);
|
|
231
|
+
}
|
|
196
232
|
[part='subtitle'] {
|
|
197
233
|
font-size: 12px;
|
|
198
234
|
color: var(--_mosaica-ink-2);
|
|
@@ -203,20 +239,41 @@ let MosaicaItemCard = (() => {
|
|
|
203
239
|
:host([compact]) .card-title {
|
|
204
240
|
font-size: 14px;
|
|
205
241
|
}
|
|
206
|
-
/*
|
|
207
|
-
|
|
208
|
-
|
|
242
|
+
/* Text kept to a few lines until Show all, or its own Show more, shows it whole: a compact
|
|
243
|
+
card's headline to three and its subtitle to two, a long value to six. */
|
|
244
|
+
.clamp {
|
|
209
245
|
display: -webkit-box;
|
|
210
|
-
-webkit-line-clamp:
|
|
211
|
-
line-clamp:
|
|
246
|
+
-webkit-line-clamp: 6;
|
|
247
|
+
line-clamp: 6;
|
|
212
248
|
-webkit-box-orient: vertical;
|
|
213
249
|
overflow: hidden;
|
|
214
250
|
overflow-wrap: anywhere;
|
|
215
251
|
}
|
|
216
|
-
|
|
252
|
+
h3[part='headline'].clamp {
|
|
253
|
+
display: -webkit-box;
|
|
254
|
+
-webkit-line-clamp: 3;
|
|
255
|
+
line-clamp: 3;
|
|
256
|
+
}
|
|
257
|
+
[part='subtitle'].clamp {
|
|
217
258
|
-webkit-line-clamp: 2;
|
|
218
259
|
line-clamp: 2;
|
|
219
260
|
}
|
|
261
|
+
/* Each is drawn as a line of text, with a target 24 px high and at least as wide around it. */
|
|
262
|
+
[part='show-all'],
|
|
263
|
+
[part='more'] {
|
|
264
|
+
position: relative;
|
|
265
|
+
}
|
|
266
|
+
[part='show-all']::before,
|
|
267
|
+
[part='more']::before {
|
|
268
|
+
content: '';
|
|
269
|
+
position: absolute;
|
|
270
|
+
inset: -4px 0;
|
|
271
|
+
min-width: 24px;
|
|
272
|
+
}
|
|
273
|
+
dl,
|
|
274
|
+
dd {
|
|
275
|
+
margin: 0;
|
|
276
|
+
}
|
|
220
277
|
:host([compact]) .foot {
|
|
221
278
|
display: flex;
|
|
222
279
|
align-items: center;
|
|
@@ -237,6 +294,7 @@ let MosaicaItemCard = (() => {
|
|
|
237
294
|
}
|
|
238
295
|
::slotted([slot='actions']) {
|
|
239
296
|
flex: none;
|
|
297
|
+
margin-left: 10px;
|
|
240
298
|
}
|
|
241
299
|
.head [part='state'] {
|
|
242
300
|
min-height: 24px;
|
|
@@ -253,14 +311,6 @@ let MosaicaItemCard = (() => {
|
|
|
253
311
|
border-radius: 6px;
|
|
254
312
|
object-fit: contain;
|
|
255
313
|
}
|
|
256
|
-
[part='field'][data-prose] [part='value'] {
|
|
257
|
-
display: -webkit-box;
|
|
258
|
-
-webkit-line-clamp: 6;
|
|
259
|
-
line-clamp: 6;
|
|
260
|
-
-webkit-box-orient: vertical;
|
|
261
|
-
overflow: hidden;
|
|
262
|
-
overflow-wrap: anywhere;
|
|
263
|
-
}
|
|
264
314
|
.field .v.mono {
|
|
265
315
|
font-size: 12px;
|
|
266
316
|
}
|
|
@@ -274,7 +324,7 @@ let MosaicaItemCard = (() => {
|
|
|
274
324
|
flex: none;
|
|
275
325
|
width: 24px;
|
|
276
326
|
height: 24px;
|
|
277
|
-
margin: -2px -4px 0
|
|
327
|
+
margin: -2px -4px 0 10px;
|
|
278
328
|
display: grid;
|
|
279
329
|
place-items: center;
|
|
280
330
|
border-radius: 5px;
|
|
@@ -289,9 +339,11 @@ let MosaicaItemCard = (() => {
|
|
|
289
339
|
gap: 6px;
|
|
290
340
|
margin: 4px 0 10px;
|
|
291
341
|
}
|
|
342
|
+
/* The bar marks the current view by more than its colour, and takes no room. */
|
|
292
343
|
[part='view-chip'][aria-current='true'] {
|
|
293
344
|
background: var(--_mosaica-accent);
|
|
294
345
|
color: var(--_mosaica-accent-ink);
|
|
346
|
+
box-shadow: inset 0 -2px 0 currentColor;
|
|
295
347
|
}
|
|
296
348
|
[part='scoped'] {
|
|
297
349
|
margin-top: 12px;
|
|
@@ -365,6 +417,19 @@ let MosaicaItemCard = (() => {
|
|
|
365
417
|
get expanded() { return this.#expanded_accessor_storage; }
|
|
366
418
|
set expanded(value) { this.#expanded_accessor_storage = value; }
|
|
367
419
|
expandedFor = (__runInitializers(this, _expanded_extraInitializers), null);
|
|
420
|
+
#opened_accessor_storage = __runInitializers(this, _opened_initializers, new Set());
|
|
421
|
+
/** The long values shown whole on the full card, by field, for the item shown. @internal */
|
|
422
|
+
get opened() { return this.#opened_accessor_storage; }
|
|
423
|
+
set opened(value) { this.#opened_accessor_storage = value; }
|
|
424
|
+
#clipped_accessor_storage = (__runInitializers(this, _opened_extraInitializers), __runInitializers(this, _clipped_initializers, new Set()));
|
|
425
|
+
/** The fields whose headline, subtitle or value is cut short where it is drawn. @internal */
|
|
426
|
+
get clipped() { return this.#clipped_accessor_storage; }
|
|
427
|
+
set clipped(value) { this.#clipped_accessor_storage = value; }
|
|
428
|
+
#copied_accessor_storage = (__runInitializers(this, _clipped_extraInitializers), __runInitializers(this, _copied_initializers, ''));
|
|
429
|
+
/** What the last press of Copy id came to, for the item shown. @internal */
|
|
430
|
+
get copied() { return this.#copied_accessor_storage; }
|
|
431
|
+
set copied(value) { this.#copied_accessor_storage = value; }
|
|
432
|
+
resized = (__runInitializers(this, _copied_extraInitializers), null);
|
|
368
433
|
#broken_accessor_storage = __runInitializers(this, _broken_initializers, new Set());
|
|
369
434
|
/** The image addresses that failed to load, shown as text. @internal */
|
|
370
435
|
get broken() { return this.#broken_accessor_storage; }
|
|
@@ -392,6 +457,9 @@ let MosaicaItemCard = (() => {
|
|
|
392
457
|
if (id !== this.expandedFor) {
|
|
393
458
|
this.expandedFor = id;
|
|
394
459
|
this.expanded = false;
|
|
460
|
+
this.opened = new Set();
|
|
461
|
+
this.clipped = new Set();
|
|
462
|
+
this.copied = '';
|
|
395
463
|
}
|
|
396
464
|
const store = this.resolvedStore;
|
|
397
465
|
if (!store || !store.get('meta') || !meta || !this.isConnected)
|
|
@@ -410,6 +478,55 @@ let MosaicaItemCard = (() => {
|
|
|
410
478
|
});
|
|
411
479
|
}
|
|
412
480
|
}
|
|
481
|
+
connectedCallback() {
|
|
482
|
+
super.connectedCallback();
|
|
483
|
+
if (typeof ResizeObserver === 'undefined')
|
|
484
|
+
return;
|
|
485
|
+
this.resized ??= new ResizeObserver(() => this.measure());
|
|
486
|
+
this.resized.observe(this);
|
|
487
|
+
}
|
|
488
|
+
disconnectedCallback() {
|
|
489
|
+
this.resized?.disconnect();
|
|
490
|
+
super.disconnectedCallback();
|
|
491
|
+
}
|
|
492
|
+
updated(changed) {
|
|
493
|
+
super.updated(changed);
|
|
494
|
+
this.measure();
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Note which clamped texts are cut short at the card's width, so each is offered whole. A text
|
|
498
|
+
* shown whole keeps what was noted while it was clamped, so the button that shows it stays.
|
|
499
|
+
*/
|
|
500
|
+
measure() {
|
|
501
|
+
const clipped = new Set(this.clipped);
|
|
502
|
+
for (const el of Array.from(this.renderRoot.querySelectorAll('.clamp'))) {
|
|
503
|
+
const name = el.closest('[data-name]')?.dataset.name;
|
|
504
|
+
if (name === undefined)
|
|
505
|
+
continue;
|
|
506
|
+
if (el.scrollHeight > el.clientHeight + 1)
|
|
507
|
+
clipped.add(name);
|
|
508
|
+
else
|
|
509
|
+
clipped.delete(name);
|
|
510
|
+
}
|
|
511
|
+
if (clipped.size !== this.clipped.size || [...clipped].some((name) => !this.clipped.has(name)))
|
|
512
|
+
this.clipped = clipped;
|
|
513
|
+
}
|
|
514
|
+
/** Copy the id to the clipboard and say in the status region how that went. */
|
|
515
|
+
async copy(id) {
|
|
516
|
+
const shown = this.expandedFor;
|
|
517
|
+
this.copied = '';
|
|
518
|
+
await this.updateComplete;
|
|
519
|
+
let copied = false;
|
|
520
|
+
try {
|
|
521
|
+
await navigator.clipboard.writeText(id);
|
|
522
|
+
copied = true;
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
// No clipboard on a page that is not secure, or the browser refused.
|
|
526
|
+
}
|
|
527
|
+
if (shown === this.expandedFor)
|
|
528
|
+
this.copied = copied ? 'Copied' : 'Could not copy';
|
|
529
|
+
}
|
|
413
530
|
get shown() {
|
|
414
531
|
// A card fed by property still takes the schema from an adopted store: declaration order and
|
|
415
532
|
// view names and frames belong to the bundle.
|
|
@@ -423,104 +540,111 @@ let MosaicaItemCard = (() => {
|
|
|
423
540
|
}
|
|
424
541
|
render() {
|
|
425
542
|
const { item, refusal, meta } = this.shown;
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const unavailable = (code, reason) => html `<div class="panel">${head(refusedState('Item unavailable', code, null, reason), true)}</div>`;
|
|
543
|
+
// The close button appears only where there is something to close.
|
|
544
|
+
let closable = true;
|
|
545
|
+
let state = html `<span part="state" data-state="shown"></span>`;
|
|
430
546
|
if (refusal)
|
|
431
|
-
|
|
432
|
-
if (!item) {
|
|
547
|
+
state = refusedState('Item unavailable', refusal.code, null, reasonOf(refusal));
|
|
548
|
+
else if (!item) {
|
|
433
549
|
const pick = this.pick;
|
|
434
550
|
// A broken pick is a fault in the layer; its details stay on the map's `lastPick`.
|
|
435
551
|
if (pick?.kind === 'broken')
|
|
436
|
-
|
|
437
|
-
if (pick?.kind === 'miss')
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
552
|
+
state = refusedState('Item unavailable', null, null, null);
|
|
553
|
+
else if (pick?.kind === 'miss')
|
|
554
|
+
state = html `<span part="state" data-state="empty">No item here</span>`;
|
|
555
|
+
else {
|
|
556
|
+
closable = false;
|
|
557
|
+
state = stateOf(this.resolvedStore?.get('status')) === 'detached' ? renderState('detached', null) : html `<span part="state" data-state="empty">No item selected</span>`;
|
|
558
|
+
}
|
|
443
559
|
}
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
const names = Object.keys(fields);
|
|
447
|
-
const ordered = [...declared.map((c) => c.name).filter((n) => n in fields), ...names.filter((n) => !declared.some((c) => c.name === n))];
|
|
448
|
-
const id = idString(item.id);
|
|
449
|
-
const titleName = this.titleField && fields[this.titleField] !== undefined && fields[this.titleField] !== null ? this.titleField : null;
|
|
450
|
-
const rest = this.namedFirst(ordered.filter((n) => n !== titleName));
|
|
451
|
-
const copy = () => void navigator.clipboard?.writeText(id);
|
|
452
|
-
if (this.compact)
|
|
453
|
-
return this.compactBody(item, meta, titleName, rest, id);
|
|
560
|
+
const card = item && !refusal ? this.card(item, meta) : null;
|
|
561
|
+
// One template whatever the card shows, so the state's region is in the page before its words change.
|
|
454
562
|
return html `<div class="panel">
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
${rest.map((name) => this.field(name, fields[name], declared.find((c) => c.name === name)))}
|
|
462
|
-
${titleName
|
|
463
|
-
? html `<div part="field" data-name="mosaica_id" style="display:contents"><span part="label" class="k">mosaica_id</span><span part="value" class="v mono">${id}</span></div>`
|
|
563
|
+
<div part="title" class="head">
|
|
564
|
+
<h2 class="sr-only">Item</h2>
|
|
565
|
+
<div class=${card ? 'said' : 'said alone'} role="status">${state}</div>
|
|
566
|
+
${card?.headline ?? nothing}
|
|
567
|
+
${closable
|
|
568
|
+
? html `<slot name="actions"></slot><button part="close" type="button" aria-label="Close" @click=${() => emit(this, 'mosaica-close', { what: 'item' })}>${icon('close', 14)}</button>`
|
|
464
569
|
: nothing}
|
|
465
570
|
</div>
|
|
466
|
-
${
|
|
467
|
-
<div class="row actions">
|
|
468
|
-
${this.openButton(id, fields, html `${icon('open', 14)}Open`)}
|
|
469
|
-
<button part="copy" class="btn" type="button" @click=${copy}>Copy id</button>
|
|
470
|
-
</div>
|
|
571
|
+
${card?.body ?? nothing}
|
|
471
572
|
</div>`;
|
|
472
573
|
}
|
|
473
|
-
/**
|
|
474
|
-
|
|
574
|
+
/** An item's headline, for the header row, and what the card shows under it: the full card's, or under `compact` the shorter one's. */
|
|
575
|
+
card(item, meta) {
|
|
475
576
|
const declared = meta?.declaredScalars ?? [];
|
|
577
|
+
const column = (name) => declared.find((c) => c.name === name);
|
|
476
578
|
const { fields } = item.detail;
|
|
477
|
-
const
|
|
579
|
+
const names = Object.keys(fields);
|
|
580
|
+
const ordered = [...declared.map((c) => c.name).filter((n) => n in fields), ...names.filter((n) => !declared.some((c) => c.name === n))];
|
|
581
|
+
const id = idString(item.id);
|
|
582
|
+
const has = (name) => fields[name] !== undefined && fields[name] !== null;
|
|
583
|
+
const titleName = this.titleField && has(this.titleField) ? this.titleField : null;
|
|
584
|
+
const title = titleName ? this.text(titleName, fields[titleName], column(titleName)) : null;
|
|
585
|
+
const rest = this.namedFirst(ordered.filter((n) => n !== titleName));
|
|
586
|
+
const whole = !this.compact || this.expanded;
|
|
587
|
+
const h3 = titleName
|
|
588
|
+
? html `<h3 part="headline" class=${whole ? 'card-title' : 'card-title clamp'} data-name=${titleName}><slot name=${`field-${titleName}`}><span part="value">${title}</span></slot></h3>`
|
|
589
|
+
: html `<h3 part="headline" class="card-title mono" data-name="mosaica_id">${id}</h3>`;
|
|
590
|
+
const idField = titleName
|
|
591
|
+
? html `<div part="field" data-name="mosaica_id" style="display:contents"><dt part="label" class="k">mosaica_id</dt><dd part="value" class="v mono">${id}</dd></div>`
|
|
592
|
+
: nothing;
|
|
593
|
+
const field = (name) => this.field(name, fields[name], column(name), title);
|
|
594
|
+
const open = this.openButton(id, fields, this.compact ? nothing : icon('open', 14));
|
|
595
|
+
if (!this.compact) {
|
|
596
|
+
return {
|
|
597
|
+
headline: h3,
|
|
598
|
+
body: html `${this.views(item.detail.views, meta)}
|
|
599
|
+
<dl class="field">${rest.map(field)}${idField}</dl>
|
|
600
|
+
${this.scoped(item.detail.scoped)}
|
|
601
|
+
<div class="row actions">
|
|
602
|
+
${open}
|
|
603
|
+
<button part="copy" class="btn" type="button" @click=${() => void this.copy(id)}>Copy id</button>
|
|
604
|
+
<span class="sr-only" role="status">${this.copied}</span>
|
|
605
|
+
</div>`
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
const subtitleName = this.subtitleField && this.subtitleField !== titleName && has(this.subtitleField) ? this.subtitleField : null;
|
|
478
609
|
const others = rest.filter((n) => n !== subtitleName);
|
|
479
610
|
const total = others.length + (titleName ? 1 : 0);
|
|
480
|
-
const
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
? html `<button part="show-all" class="more-link" type="button" aria-expanded=${this.expanded ? 'true' : 'false'} @click=${() => (this.expanded = !this.expanded)}>${this.expanded ? 'Show fewer' : `Show all ${total.toLocaleString('en-GB')} fields`}</button>`
|
|
611
|
+
const few = this.cardFields ? this.named(others) : others.slice(0, 3);
|
|
612
|
+
const more = total > few.length;
|
|
613
|
+
// Show all is there for the fields left out, and for any text the card cuts short.
|
|
614
|
+
const showAll = more || this.expanded || this.clipped.size > 0
|
|
615
|
+
? html `<button part="show-all" class="more-link" type="button" aria-expanded=${this.expanded ? 'true' : 'false'} @click=${() => (this.expanded = !this.expanded)}>${this.expanded ? (more ? 'Show fewer' : 'Show less') : more ? `Show all ${total.toLocaleString('en-GB')} fields` : 'Show all'}</button>`
|
|
486
616
|
: null;
|
|
487
|
-
const open = this.openButton(id, fields, 'Open');
|
|
488
617
|
const subtitle = subtitleName
|
|
489
|
-
? html `<div part="subtitle" data-name=${subtitleName}><slot name=${`field-${subtitleName}`}>${this.text(subtitleName, fields[subtitleName],
|
|
618
|
+
? html `<div part="subtitle" class=${whole ? '' : 'clamp'} data-name=${subtitleName}><slot name=${`field-${subtitleName}`}>${this.text(subtitleName, fields[subtitleName], column(subtitleName))}</slot></div>`
|
|
490
619
|
: nothing;
|
|
491
|
-
return
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
${shown.map((name) => this.field(name, fields[name], declared.find((c) => c.name === name)))}
|
|
498
|
-
${this.expanded && titleName
|
|
499
|
-
? html `<div part="field" data-name="mosaica_id" style="display:contents"><span part="label" class="k">mosaica_id</span><span part="value" class="v mono">${id}</span></div>`
|
|
500
|
-
: nothing}
|
|
620
|
+
return {
|
|
621
|
+
headline: html `<div class="headline">${h3}${subtitle}</div>`,
|
|
622
|
+
body: html `<div class="body">
|
|
623
|
+
${this.expanded ? this.views(item.detail.views, meta) : nothing}
|
|
624
|
+
<dl class="field">${(this.expanded ? others : few).map(field)}${this.expanded ? idField : nothing}</dl>
|
|
625
|
+
${this.expanded ? this.scoped(item.detail.scoped) : nothing}
|
|
501
626
|
</div>
|
|
502
|
-
${
|
|
503
|
-
|
|
504
|
-
${showAll || open !== nothing ? html `<div class="foot">${showAll || html `<span></span>`}${open}</div>` : nothing}
|
|
505
|
-
</div>`;
|
|
627
|
+
${showAll || open !== nothing ? html `<div class="foot">${showAll || html `<span></span>`}${open}</div>` : nothing}`
|
|
628
|
+
};
|
|
506
629
|
}
|
|
507
630
|
/**
|
|
508
631
|
* Open, where the host says what it does: a button that calls `open`, else a link to `open-url`
|
|
509
|
-
* filled from the item, else nothing.
|
|
632
|
+
* filled from the item, which opens in a new tab and says so to a screen reader, else nothing.
|
|
633
|
+
* Each fires `mosaica-open`. `mark` is drawn before the word.
|
|
510
634
|
*/
|
|
511
|
-
openButton(id, fields,
|
|
635
|
+
openButton(id, fields, mark) {
|
|
512
636
|
const pressed = () => emit(this, 'mosaica-open', { id, fields });
|
|
513
637
|
const open = this.open;
|
|
514
638
|
if (open)
|
|
515
639
|
return html `<button part="open" class="btn" type="button" @click=${() => {
|
|
516
640
|
pressed();
|
|
517
641
|
open({ id, fields });
|
|
518
|
-
}}>${
|
|
642
|
+
}}>${mark}Open</button>`;
|
|
519
643
|
const href = this.openUrl ? openAddress(this.openUrl, { mosaica_id: id, ...fields }) : null;
|
|
520
644
|
if (!href)
|
|
521
645
|
return nothing;
|
|
522
646
|
// A middle-click opens the link too, and is a press.
|
|
523
|
-
return html `<a part="open" class="btn" href=${href} target="_blank" rel="noopener noreferrer" @click=${pressed} @auxclick=${(e) => e.button === 1 && pressed()}>${
|
|
647
|
+
return html `<a part="open" class="btn" href=${href} target="_blank" rel="noopener noreferrer" @click=${pressed} @auxclick=${(e) => e.button === 1 && pressed()}>${mark}Open<span class="sr-only"> (opens in a new tab)</span></a>`;
|
|
524
648
|
}
|
|
525
649
|
/** The fields of `fields` that `card-fields` names, in its order. */
|
|
526
650
|
named(fields) {
|
|
@@ -549,8 +673,8 @@ let MosaicaItemCard = (() => {
|
|
|
549
673
|
if (positions.length === 0)
|
|
550
674
|
return nothing;
|
|
551
675
|
const current = this.resolvedStore?.get('view').id ?? '';
|
|
552
|
-
return html `<span part="label" class="xs muted">In views</span>
|
|
553
|
-
<div class="chips">
|
|
676
|
+
return html `<span part="label" class="xs muted" id="views">In views</span>
|
|
677
|
+
<div class="chips" role="group" aria-labelledby="views">
|
|
554
678
|
${positions.map((p) => {
|
|
555
679
|
const view = meta?.views.find((v) => v.id === p.id) ?? null;
|
|
556
680
|
const here = p.id === current;
|
|
@@ -586,30 +710,39 @@ let MosaicaItemCard = (() => {
|
|
|
586
710
|
if (keys.length === 0)
|
|
587
711
|
return nothing;
|
|
588
712
|
return html `<div part="scoped">
|
|
589
|
-
${keys.map((key) => html `<
|
|
590
|
-
<
|
|
713
|
+
${keys.map((key) => html `<h4 part="key" class="hd" data-key=${key}>${key}</h4>
|
|
714
|
+
<dl class="field" data-key=${key}>
|
|
591
715
|
${Object.keys(scoped)
|
|
592
716
|
.filter((family) => key in (scoped[family] ?? {}))
|
|
593
|
-
.map((family) => html `<div part="field" data-name=${family} data-key=${key} style="display:contents"><
|
|
594
|
-
</
|
|
717
|
+
.map((family) => html `<div part="field" data-name=${family} data-key=${key} style="display:contents"><dt part="label" class="k">${columnCaption(family)}</dt><dd part="value" class="v">${present(scoped[family][key], undefined)}</dd></div>`)}
|
|
718
|
+
</dl>`)}
|
|
595
719
|
</div>`;
|
|
596
720
|
}
|
|
597
|
-
/**
|
|
598
|
-
|
|
721
|
+
/**
|
|
722
|
+
* One field, presented by its declared type; prose and images span the grid. An image's
|
|
723
|
+
* alternative text is the item's `headline` where it has one, since its caption is the label
|
|
724
|
+
* above it. On the full card, prose a clamp cuts short has a button that shows it whole.
|
|
725
|
+
*/
|
|
726
|
+
field(name, value, column, headline) {
|
|
599
727
|
const caption = column ? columnCaption(name) : name;
|
|
600
728
|
const image = this.imageOf(name, value);
|
|
601
729
|
if (image) {
|
|
602
730
|
const failed = () => (this.broken = new Set([...this.broken, image]));
|
|
603
731
|
return html `<div part="field" data-name=${name} data-image>
|
|
604
|
-
<
|
|
605
|
-
<slot name=${`field-${name}`}><img part="image" src=${image} alt=${caption} loading="lazy" referrerpolicy="no-referrer" @error=${failed} /></slot>
|
|
732
|
+
<dt part="label" class="k">${caption}</dt>
|
|
733
|
+
<dd part="value" class="v"><slot name=${`field-${name}`}><img part="image" src=${image} alt=${headline ?? caption} loading="lazy" referrerpolicy="no-referrer" @error=${failed} /></slot></dd>
|
|
606
734
|
</div>`;
|
|
607
735
|
}
|
|
608
736
|
const text = this.text(name, value, column);
|
|
609
737
|
const prose = text.length > 60;
|
|
738
|
+
const whole = this.compact ? this.expanded : this.opened.has(name);
|
|
739
|
+
const toggle = () => (this.opened = new Set(whole ? [...this.opened].filter((n) => n !== name) : [...this.opened, name]));
|
|
610
740
|
return html `<div part="field" data-name=${name} ?data-prose=${prose} style=${prose ? nothing : 'display:contents'}>
|
|
611
|
-
<
|
|
612
|
-
<
|
|
741
|
+
<dt part="label" class="k">${caption}</dt>
|
|
742
|
+
<dd part="value" class="v" title=${prose ? text : nothing}><slot name=${`field-${name}`}>${prose && !whole ? html `<span class="clamp">${text}</span>` : text}</slot></dd>
|
|
743
|
+
${prose && !this.compact && (whole || this.clipped.has(name))
|
|
744
|
+
? html `<dd><button part="more" class="more-link" type="button" aria-expanded=${whole ? 'true' : 'false'} aria-label=${`${whole ? 'Show less' : 'Show more'} of ${caption}`} @click=${toggle}>${whole ? 'Show less' : 'Show more'}</button></dd>`
|
|
745
|
+
: nothing}
|
|
613
746
|
</div>`;
|
|
614
747
|
}
|
|
615
748
|
};
|
|
@@ -658,7 +791,7 @@ function present(value, column) {
|
|
|
658
791
|
if (typeof value === 'number')
|
|
659
792
|
return Number.isInteger(value) ? value.toLocaleString('en-GB') : String(value);
|
|
660
793
|
if (typeof value === 'boolean')
|
|
661
|
-
return value ? '
|
|
794
|
+
return BOOL_WORDS[value ? 'true' : 'false'];
|
|
662
795
|
return String(value);
|
|
663
796
|
}
|
|
664
797
|
attachContextRoot();
|
package/dist/key-picker.d.ts
CHANGED
|
@@ -14,7 +14,8 @@ import { MosaicaElement } from './base.js';
|
|
|
14
14
|
* @csspart label - The caption, the group's name.
|
|
15
15
|
* @csspart entry - The row of buttons and select.
|
|
16
16
|
* @csspart select - The select.
|
|
17
|
-
* @csspart step - A previous or next button, with `data-direction` set to
|
|
17
|
+
* @csspart step - A previous or next button, named with the group, with `data-direction` set to
|
|
18
|
+
* `prev` or `next`.
|
|
18
19
|
*/
|
|
19
20
|
export declare class MosaicaKeyPicker extends MosaicaElement {
|
|
20
21
|
static styles: import("lit").CSSResult[];
|
package/dist/key-picker.js
CHANGED
|
@@ -19,7 +19,8 @@ import { chrome, tokens } from './tokens.js';
|
|
|
19
19
|
* @csspart label - The caption, the group's name.
|
|
20
20
|
* @csspart entry - The row of buttons and select.
|
|
21
21
|
* @csspart select - The select.
|
|
22
|
-
* @csspart step - A previous or next button, with `data-direction` set to
|
|
22
|
+
* @csspart step - A previous or next button, named with the group, with `data-direction` set to
|
|
23
|
+
* `prev` or `next`.
|
|
23
24
|
*/
|
|
24
25
|
export class MosaicaKeyPicker extends MosaicaElement {
|
|
25
26
|
static styles = [
|
|
@@ -107,11 +108,11 @@ export class MosaicaKeyPicker extends MosaicaElement {
|
|
|
107
108
|
return html `<div part="field">
|
|
108
109
|
<span part="label" class="muted">${heading}</span>
|
|
109
110
|
<div part="entry" class="row">
|
|
110
|
-
${step('prev', previous, -1,
|
|
111
|
+
${step('prev', previous, -1, `Previous ${heading}`)}
|
|
111
112
|
<select part="select" aria-label=${heading} @change=${(e) => switchView(this, s, meta, e.target.value)}>
|
|
112
113
|
${views.map((v) => html `<option value=${v.id} ?selected=${v.id === current.id}>${this.option(meta, v)}</option>`)}
|
|
113
114
|
</select>
|
|
114
|
-
${step('next', next, 1,
|
|
115
|
+
${step('next', next, 1, `Next ${heading}`)}
|
|
115
116
|
</div>
|
|
116
117
|
</div>`;
|
|
117
118
|
}
|
package/dist/layer-picker.d.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { MosaicaElement } from './base.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* Put the layer picker beside a map when the bundle has annotation layers that viewers should be
|
|
5
5
|
* able to turn on and off. A layer that others depend on, such as a clustering and its labels, has
|
|
6
|
-
* one checkbox, so that the whole group is turned on together.
|
|
6
|
+
* one checkbox, so that the whole group is turned on together. The row's `title` names the layers
|
|
7
|
+
* it turns on, and a screen reader hears them as the checkbox's description.
|
|
7
8
|
*
|
|
8
9
|
* Filter layers draw nothing, so the picker names them in a note instead of giving them checkboxes.
|
|
9
10
|
* Viewers filter by a filter layer from its card in `<mosaica-filter-panel>`, or browse it in
|
|
@@ -19,7 +20,8 @@ import { MosaicaElement } from './base.js';
|
|
|
19
20
|
* @csspart refusal - The words "View refused", with `data-code`, in the refused state.
|
|
20
21
|
* @csspart reason - The server's reason for a refusal, under "View refused".
|
|
21
22
|
* @csspart entry - One layer that draws, with `data-layer`.
|
|
22
|
-
* @csspart name - A layer's title, or its name if it has no title
|
|
23
|
+
* @csspart name - A layer's title, or its name if it has no title, on one line unless
|
|
24
|
+
* `--mosaica-long-text` is `normal`.
|
|
23
25
|
* @csspart note - The note naming the filter layers, such as "Venues can be used as a filter
|
|
24
26
|
* only.", with `data-layers` listing their names.
|
|
25
27
|
*/
|