@mosaicajs/components 0.2.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 +118 -37
- package/dist/explorer.js +487 -212
- 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 +62 -14
- package/dist/item-card.js +310 -110
- 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 +1848 -832
- 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,14 +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.
|
|
56
|
+
*
|
|
57
|
+
* A field named in `image-fields` whose value is an `http:` or `https:` address, or a path
|
|
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
|
|
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.
|
|
51
62
|
*
|
|
52
63
|
* The Open button appears only when you set `open-url` or `open`. `open-url` is a link template
|
|
53
64
|
* such as `https://www.geonames.org/{geonameid}`, in which each `{name}` is replaced by the item's
|
|
54
65
|
* value for that field. Because the values are URL-encoded, a template cannot open an address that
|
|
55
66
|
* a field already holds, so set `open` to a function for that. An item gets no Open button when it
|
|
56
|
-
* has no value for a field the template names. A function in `open`
|
|
57
|
-
* `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`.
|
|
58
69
|
*
|
|
59
70
|
* @summary A card showing an item's fields, with an optional Open button.
|
|
60
71
|
* @tagname mosaica-item-card
|
|
@@ -69,22 +80,26 @@ import { chrome, tokens } from './tokens.js';
|
|
|
69
80
|
* @csspart title - The header row: the headline or the state, and the close button.
|
|
70
81
|
* @csspart close - The close button.
|
|
71
82
|
* @csspart state - The state line, with `data-state`: `shown`, `empty` (no item, or a click that
|
|
72
|
-
* found nothing), `refused` or `detached`.
|
|
83
|
+
* found nothing), `refused` or `detached`. It is inside a `role="status"` region.
|
|
73
84
|
* @csspart refusal - The words "Item unavailable", with `data-code` set to the refusal's code if
|
|
74
85
|
* the server refused the record. For a broken pick, the details are in the map's `lastPick`.
|
|
75
86
|
* @csspart reason - The server's reason for refusing the record, under "Item unavailable".
|
|
76
|
-
* @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.
|
|
77
88
|
* @csspart subtitle - The `subtitle-field` value under the headline, when `compact` is set.
|
|
78
89
|
* @csspart show-all - The "Show all N fields" button, when `compact` is set, with `aria-expanded`.
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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.
|
|
81
93
|
* @csspart field - One field, with `data-name`, and `data-prose` on a value over 60 characters.
|
|
82
|
-
* @csspart label - A field's name, and the heading above the view chips.
|
|
83
|
-
* @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`.
|
|
97
|
+
* @csspart image - A field's value drawn as an image, under `image-fields`.
|
|
84
98
|
* @csspart scoped - The group-scoped values, grouped by key.
|
|
85
99
|
* @csspart key - One key's heading among the group-scoped values, with `data-key`.
|
|
86
100
|
* @csspart open - The Open button.
|
|
87
|
-
* @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.
|
|
88
103
|
*/
|
|
89
104
|
let MosaicaItemCard = (() => {
|
|
90
105
|
let _classSuper = MosaicaElement;
|
|
@@ -112,6 +127,9 @@ let MosaicaItemCard = (() => {
|
|
|
112
127
|
let _cardFields_decorators;
|
|
113
128
|
let _cardFields_initializers = [];
|
|
114
129
|
let _cardFields_extraInitializers = [];
|
|
130
|
+
let _imageFields_decorators;
|
|
131
|
+
let _imageFields_initializers = [];
|
|
132
|
+
let _imageFields_extraInitializers = [];
|
|
115
133
|
let _openUrl_decorators;
|
|
116
134
|
let _openUrl_initializers = [];
|
|
117
135
|
let _openUrl_extraInitializers = [];
|
|
@@ -121,6 +139,18 @@ let MosaicaItemCard = (() => {
|
|
|
121
139
|
let _expanded_decorators;
|
|
122
140
|
let _expanded_initializers = [];
|
|
123
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 = [];
|
|
151
|
+
let _broken_decorators;
|
|
152
|
+
let _broken_initializers = [];
|
|
153
|
+
let _broken_extraInitializers = [];
|
|
124
154
|
return class MosaicaItemCard extends _classSuper {
|
|
125
155
|
static {
|
|
126
156
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
@@ -132,9 +162,14 @@ let MosaicaItemCard = (() => {
|
|
|
132
162
|
_compact_decorators = [property({ type: Boolean, reflect: true })];
|
|
133
163
|
_subtitleField_decorators = [property({ attribute: 'subtitle-field' })];
|
|
134
164
|
_cardFields_decorators = [property({ attribute: 'card-fields' })];
|
|
165
|
+
_imageFields_decorators = [property({ attribute: 'image-fields' })];
|
|
135
166
|
_openUrl_decorators = [property({ attribute: 'open-url' })];
|
|
136
167
|
_open_decorators = [property({ attribute: false })];
|
|
137
168
|
_expanded_decorators = [state()];
|
|
169
|
+
_opened_decorators = [state()];
|
|
170
|
+
_clipped_decorators = [state()];
|
|
171
|
+
_copied_decorators = [state()];
|
|
172
|
+
_broken_decorators = [state()];
|
|
138
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);
|
|
139
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);
|
|
140
175
|
__esDecorate(this, null, _pick_decorators, { kind: "accessor", name: "pick", static: false, private: false, access: { has: obj => "pick" in obj, get: obj => obj.pick, set: (obj, value) => { obj.pick = value; } }, metadata: _metadata }, _pick_initializers, _pick_extraInitializers);
|
|
@@ -143,9 +178,14 @@ let MosaicaItemCard = (() => {
|
|
|
143
178
|
__esDecorate(this, null, _compact_decorators, { kind: "accessor", name: "compact", static: false, private: false, access: { has: obj => "compact" in obj, get: obj => obj.compact, set: (obj, value) => { obj.compact = value; } }, metadata: _metadata }, _compact_initializers, _compact_extraInitializers);
|
|
144
179
|
__esDecorate(this, null, _subtitleField_decorators, { kind: "accessor", name: "subtitleField", static: false, private: false, access: { has: obj => "subtitleField" in obj, get: obj => obj.subtitleField, set: (obj, value) => { obj.subtitleField = value; } }, metadata: _metadata }, _subtitleField_initializers, _subtitleField_extraInitializers);
|
|
145
180
|
__esDecorate(this, null, _cardFields_decorators, { kind: "accessor", name: "cardFields", static: false, private: false, access: { has: obj => "cardFields" in obj, get: obj => obj.cardFields, set: (obj, value) => { obj.cardFields = value; } }, metadata: _metadata }, _cardFields_initializers, _cardFields_extraInitializers);
|
|
181
|
+
__esDecorate(this, null, _imageFields_decorators, { kind: "accessor", name: "imageFields", static: false, private: false, access: { has: obj => "imageFields" in obj, get: obj => obj.imageFields, set: (obj, value) => { obj.imageFields = value; } }, metadata: _metadata }, _imageFields_initializers, _imageFields_extraInitializers);
|
|
146
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);
|
|
147
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);
|
|
148
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);
|
|
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);
|
|
149
189
|
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
150
190
|
}
|
|
151
191
|
static styles = [
|
|
@@ -159,7 +199,7 @@ let MosaicaItemCard = (() => {
|
|
|
159
199
|
display: flex;
|
|
160
200
|
align-items: flex-start;
|
|
161
201
|
justify-content: space-between;
|
|
162
|
-
gap:
|
|
202
|
+
gap: 0;
|
|
163
203
|
margin: 0 0 10px;
|
|
164
204
|
font-size: 13px;
|
|
165
205
|
font-weight: 400;
|
|
@@ -167,6 +207,11 @@ let MosaicaItemCard = (() => {
|
|
|
167
207
|
text-transform: none;
|
|
168
208
|
color: var(--_mosaica-ink);
|
|
169
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,
|
|
170
215
|
.head [part='headline'],
|
|
171
216
|
.head .headline {
|
|
172
217
|
flex: 1 1 auto;
|
|
@@ -177,6 +222,13 @@ let MosaicaItemCard = (() => {
|
|
|
177
222
|
flex-direction: column;
|
|
178
223
|
gap: 3px;
|
|
179
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
|
+
}
|
|
180
232
|
[part='subtitle'] {
|
|
181
233
|
font-size: 12px;
|
|
182
234
|
color: var(--_mosaica-ink-2);
|
|
@@ -187,20 +239,41 @@ let MosaicaItemCard = (() => {
|
|
|
187
239
|
:host([compact]) .card-title {
|
|
188
240
|
font-size: 14px;
|
|
189
241
|
}
|
|
190
|
-
/*
|
|
191
|
-
|
|
192
|
-
|
|
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 {
|
|
193
245
|
display: -webkit-box;
|
|
194
|
-
-webkit-line-clamp:
|
|
195
|
-
line-clamp:
|
|
246
|
+
-webkit-line-clamp: 6;
|
|
247
|
+
line-clamp: 6;
|
|
196
248
|
-webkit-box-orient: vertical;
|
|
197
249
|
overflow: hidden;
|
|
198
250
|
overflow-wrap: anywhere;
|
|
199
251
|
}
|
|
200
|
-
|
|
252
|
+
h3[part='headline'].clamp {
|
|
253
|
+
display: -webkit-box;
|
|
254
|
+
-webkit-line-clamp: 3;
|
|
255
|
+
line-clamp: 3;
|
|
256
|
+
}
|
|
257
|
+
[part='subtitle'].clamp {
|
|
201
258
|
-webkit-line-clamp: 2;
|
|
202
259
|
line-clamp: 2;
|
|
203
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
|
+
}
|
|
204
277
|
:host([compact]) .foot {
|
|
205
278
|
display: flex;
|
|
206
279
|
align-items: center;
|
|
@@ -221,20 +294,22 @@ let MosaicaItemCard = (() => {
|
|
|
221
294
|
}
|
|
222
295
|
::slotted([slot='actions']) {
|
|
223
296
|
flex: none;
|
|
297
|
+
margin-left: 10px;
|
|
224
298
|
}
|
|
225
299
|
.head [part='state'] {
|
|
226
300
|
min-height: 24px;
|
|
227
301
|
}
|
|
228
|
-
[part='field'][data-prose]
|
|
302
|
+
[part='field'][data-prose],
|
|
303
|
+
[part='field'][data-image] {
|
|
229
304
|
grid-column: 1 / -1;
|
|
230
305
|
}
|
|
231
|
-
[part='
|
|
232
|
-
display:
|
|
233
|
-
-
|
|
234
|
-
|
|
235
|
-
-
|
|
236
|
-
|
|
237
|
-
|
|
306
|
+
[part='image'] {
|
|
307
|
+
display: block;
|
|
308
|
+
max-width: 100%;
|
|
309
|
+
max-height: 240px;
|
|
310
|
+
margin-top: 4px;
|
|
311
|
+
border-radius: 6px;
|
|
312
|
+
object-fit: contain;
|
|
238
313
|
}
|
|
239
314
|
.field .v.mono {
|
|
240
315
|
font-size: 12px;
|
|
@@ -249,7 +324,7 @@ let MosaicaItemCard = (() => {
|
|
|
249
324
|
flex: none;
|
|
250
325
|
width: 24px;
|
|
251
326
|
height: 24px;
|
|
252
|
-
margin: -2px -4px 0
|
|
327
|
+
margin: -2px -4px 0 10px;
|
|
253
328
|
display: grid;
|
|
254
329
|
place-items: center;
|
|
255
330
|
border-radius: 5px;
|
|
@@ -264,9 +339,11 @@ let MosaicaItemCard = (() => {
|
|
|
264
339
|
gap: 6px;
|
|
265
340
|
margin: 4px 0 10px;
|
|
266
341
|
}
|
|
342
|
+
/* The bar marks the current view by more than its colour, and takes no room. */
|
|
267
343
|
[part='view-chip'][aria-current='true'] {
|
|
268
344
|
background: var(--_mosaica-accent);
|
|
269
345
|
color: var(--_mosaica-accent-ink);
|
|
346
|
+
box-shadow: inset 0 -2px 0 currentColor;
|
|
270
347
|
}
|
|
271
348
|
[part='scoped'] {
|
|
272
349
|
margin-top: 12px;
|
|
@@ -317,7 +394,14 @@ let MosaicaItemCard = (() => {
|
|
|
317
394
|
*/
|
|
318
395
|
get cardFields() { return this.#cardFields_accessor_storage; }
|
|
319
396
|
set cardFields(value) { this.#cardFields_accessor_storage = value; }
|
|
320
|
-
#
|
|
397
|
+
#imageFields_accessor_storage = (__runInitializers(this, _cardFields_extraInitializers), __runInitializers(this, _imageFields_initializers, ''));
|
|
398
|
+
/**
|
|
399
|
+
* The fields whose value is the address of an image, separated by spaces or commas. Each is drawn
|
|
400
|
+
* as its image rather than as text.
|
|
401
|
+
*/
|
|
402
|
+
get imageFields() { return this.#imageFields_accessor_storage; }
|
|
403
|
+
set imageFields(value) { this.#imageFields_accessor_storage = value; }
|
|
404
|
+
#openUrl_accessor_storage = (__runInitializers(this, _imageFields_extraInitializers), __runInitializers(this, _openUrl_initializers, ''));
|
|
321
405
|
/**
|
|
322
406
|
* The link template for Open, with `{name}` for the value of the field `name` and `{mosaica_id}`
|
|
323
407
|
* for the item's id. Without it or `open`, there is no Open button.
|
|
@@ -333,6 +417,25 @@ let MosaicaItemCard = (() => {
|
|
|
333
417
|
get expanded() { return this.#expanded_accessor_storage; }
|
|
334
418
|
set expanded(value) { this.#expanded_accessor_storage = value; }
|
|
335
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);
|
|
433
|
+
#broken_accessor_storage = __runInitializers(this, _broken_initializers, new Set());
|
|
434
|
+
/** The image addresses that failed to load, shown as text. @internal */
|
|
435
|
+
get broken() { return this.#broken_accessor_storage; }
|
|
436
|
+
set broken(value) { this.#broken_accessor_storage = value; }
|
|
437
|
+
/** `image-fields` as a list, kept in step by `willUpdate`. */
|
|
438
|
+
imageNames = (__runInitializers(this, _broken_extraInitializers), []);
|
|
336
439
|
/** A category value's title as the store holds it, else from the legend or a suggestion page, else its key. */
|
|
337
440
|
titleOf(field, key) {
|
|
338
441
|
return this.resolvedStore?.heldTitle(field, key) ?? keyTitle(this.resolvedStore, field, key);
|
|
@@ -347,11 +450,16 @@ let MosaicaItemCard = (() => {
|
|
|
347
450
|
*/
|
|
348
451
|
willUpdate(changed) {
|
|
349
452
|
super.willUpdate(changed);
|
|
453
|
+
if (changed.has('imageFields'))
|
|
454
|
+
this.imageNames = listed(this.imageFields);
|
|
350
455
|
const { item, meta } = this.shown;
|
|
351
456
|
const id = item?.id ?? null;
|
|
352
457
|
if (id !== this.expandedFor) {
|
|
353
458
|
this.expandedFor = id;
|
|
354
459
|
this.expanded = false;
|
|
460
|
+
this.opened = new Set();
|
|
461
|
+
this.clipped = new Set();
|
|
462
|
+
this.copied = '';
|
|
355
463
|
}
|
|
356
464
|
const store = this.resolvedStore;
|
|
357
465
|
if (!store || !store.get('meta') || !meta || !this.isConnected)
|
|
@@ -370,6 +478,55 @@ let MosaicaItemCard = (() => {
|
|
|
370
478
|
});
|
|
371
479
|
}
|
|
372
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
|
+
}
|
|
373
530
|
get shown() {
|
|
374
531
|
// A card fed by property still takes the schema from an adopted store: declaration order and
|
|
375
532
|
// view names and frames belong to the bundle.
|
|
@@ -383,109 +540,123 @@ let MosaicaItemCard = (() => {
|
|
|
383
540
|
}
|
|
384
541
|
render() {
|
|
385
542
|
const { item, refusal, meta } = this.shown;
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
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>`;
|
|
390
546
|
if (refusal)
|
|
391
|
-
|
|
392
|
-
if (!item) {
|
|
547
|
+
state = refusedState('Item unavailable', refusal.code, null, reasonOf(refusal));
|
|
548
|
+
else if (!item) {
|
|
393
549
|
const pick = this.pick;
|
|
394
550
|
// A broken pick is a fault in the layer; its details stay on the map's `lastPick`.
|
|
395
551
|
if (pick?.kind === 'broken')
|
|
396
|
-
|
|
397
|
-
if (pick?.kind === 'miss')
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
+
}
|
|
403
559
|
}
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
const names = Object.keys(fields);
|
|
407
|
-
const ordered = [...declared.map((c) => c.name).filter((n) => n in fields), ...names.filter((n) => !declared.some((c) => c.name === n))];
|
|
408
|
-
const id = idString(item.id);
|
|
409
|
-
const titleName = this.titleField && fields[this.titleField] !== undefined && fields[this.titleField] !== null ? this.titleField : null;
|
|
410
|
-
const rest = this.namedFirst(ordered.filter((n) => n !== titleName));
|
|
411
|
-
const copy = () => void navigator.clipboard?.writeText(id);
|
|
412
|
-
if (this.compact)
|
|
413
|
-
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.
|
|
414
562
|
return html `<div class="panel">
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
${rest.map((name) => this.field(name, fields[name], declared.find((c) => c.name === name)))}
|
|
422
|
-
${titleName
|
|
423
|
-
? 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>`
|
|
424
569
|
: nothing}
|
|
425
570
|
</div>
|
|
426
|
-
${
|
|
427
|
-
<div class="row actions">
|
|
428
|
-
${this.openButton(id, fields, html `${icon('open', 14)}Open`)}
|
|
429
|
-
<button part="copy" class="btn" type="button" @click=${copy}>Copy id</button>
|
|
430
|
-
</div>
|
|
571
|
+
${card?.body ?? nothing}
|
|
431
572
|
</div>`;
|
|
432
573
|
}
|
|
433
|
-
/**
|
|
434
|
-
|
|
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) {
|
|
435
576
|
const declared = meta?.declaredScalars ?? [];
|
|
577
|
+
const column = (name) => declared.find((c) => c.name === name);
|
|
436
578
|
const { fields } = item.detail;
|
|
437
|
-
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;
|
|
438
609
|
const others = rest.filter((n) => n !== subtitleName);
|
|
439
610
|
const total = others.length + (titleName ? 1 : 0);
|
|
440
|
-
const
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
? 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>`
|
|
446
616
|
: null;
|
|
447
|
-
const open = this.openButton(id, fields, 'Open');
|
|
448
617
|
const subtitle = subtitleName
|
|
449
|
-
? 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>`
|
|
450
619
|
: nothing;
|
|
451
|
-
return
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
${shown.map((name) => this.field(name, fields[name], declared.find((c) => c.name === name)))}
|
|
458
|
-
${this.expanded && titleName
|
|
459
|
-
? 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>`
|
|
460
|
-
: 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}
|
|
461
626
|
</div>
|
|
462
|
-
${
|
|
463
|
-
|
|
464
|
-
${showAll || open !== nothing ? html `<div class="foot">${showAll || html `<span></span>`}${open}</div>` : nothing}
|
|
465
|
-
</div>`;
|
|
627
|
+
${showAll || open !== nothing ? html `<div class="foot">${showAll || html `<span></span>`}${open}</div>` : nothing}`
|
|
628
|
+
};
|
|
466
629
|
}
|
|
467
630
|
/**
|
|
468
631
|
* Open, where the host says what it does: a button that calls `open`, else a link to `open-url`
|
|
469
|
-
* 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.
|
|
470
634
|
*/
|
|
471
|
-
openButton(id, fields,
|
|
635
|
+
openButton(id, fields, mark) {
|
|
472
636
|
const pressed = () => emit(this, 'mosaica-open', { id, fields });
|
|
473
637
|
const open = this.open;
|
|
474
638
|
if (open)
|
|
475
639
|
return html `<button part="open" class="btn" type="button" @click=${() => {
|
|
476
640
|
pressed();
|
|
477
641
|
open({ id, fields });
|
|
478
|
-
}}>${
|
|
642
|
+
}}>${mark}Open</button>`;
|
|
479
643
|
const href = this.openUrl ? openAddress(this.openUrl, { mosaica_id: id, ...fields }) : null;
|
|
480
644
|
if (!href)
|
|
481
645
|
return nothing;
|
|
482
646
|
// A middle-click opens the link too, and is a press.
|
|
483
|
-
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>`;
|
|
484
648
|
}
|
|
485
649
|
/** The fields of `fields` that `card-fields` names, in its order. */
|
|
486
650
|
named(fields) {
|
|
487
|
-
const names = this.cardFields
|
|
488
|
-
return names.filter((n, i) =>
|
|
651
|
+
const names = listed(this.cardFields);
|
|
652
|
+
return names.filter((n, i) => fields.includes(n) && names.indexOf(n) === i);
|
|
653
|
+
}
|
|
654
|
+
/** The address of the image a field's value names, where `image-fields` lists the field. */
|
|
655
|
+
imageOf(name, value) {
|
|
656
|
+
if (typeof value !== 'string' || !this.imageNames.includes(name))
|
|
657
|
+
return null;
|
|
658
|
+
const address = webAddress(value);
|
|
659
|
+
return address && !this.broken.has(address) ? address : null;
|
|
489
660
|
}
|
|
490
661
|
/** `fields` with those `card-fields` names first, in its order. */
|
|
491
662
|
namedFirst(fields) {
|
|
@@ -502,8 +673,8 @@ let MosaicaItemCard = (() => {
|
|
|
502
673
|
if (positions.length === 0)
|
|
503
674
|
return nothing;
|
|
504
675
|
const current = this.resolvedStore?.get('view').id ?? '';
|
|
505
|
-
return html `<span part="label" class="xs muted">In views</span>
|
|
506
|
-
<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">
|
|
507
678
|
${positions.map((p) => {
|
|
508
679
|
const view = meta?.views.find((v) => v.id === p.id) ?? null;
|
|
509
680
|
const here = p.id === current;
|
|
@@ -539,21 +710,39 @@ let MosaicaItemCard = (() => {
|
|
|
539
710
|
if (keys.length === 0)
|
|
540
711
|
return nothing;
|
|
541
712
|
return html `<div part="scoped">
|
|
542
|
-
${keys.map((key) => html `<
|
|
543
|
-
<
|
|
713
|
+
${keys.map((key) => html `<h4 part="key" class="hd" data-key=${key}>${key}</h4>
|
|
714
|
+
<dl class="field" data-key=${key}>
|
|
544
715
|
${Object.keys(scoped)
|
|
545
716
|
.filter((family) => key in (scoped[family] ?? {}))
|
|
546
|
-
.map((family) => html `<div part="field" data-name=${family} data-key=${key} style="display:contents"><
|
|
547
|
-
</
|
|
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>`)}
|
|
548
719
|
</div>`;
|
|
549
720
|
}
|
|
550
|
-
/**
|
|
551
|
-
|
|
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) {
|
|
727
|
+
const caption = column ? columnCaption(name) : name;
|
|
728
|
+
const image = this.imageOf(name, value);
|
|
729
|
+
if (image) {
|
|
730
|
+
const failed = () => (this.broken = new Set([...this.broken, image]));
|
|
731
|
+
return html `<div part="field" data-name=${name} data-image>
|
|
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>
|
|
734
|
+
</div>`;
|
|
735
|
+
}
|
|
552
736
|
const text = this.text(name, value, column);
|
|
553
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]));
|
|
554
740
|
return html `<div part="field" data-name=${name} ?data-prose=${prose} style=${prose ? nothing : 'display:contents'}>
|
|
555
|
-
<
|
|
556
|
-
<
|
|
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}
|
|
557
746
|
</div>`;
|
|
558
747
|
}
|
|
559
748
|
};
|
|
@@ -573,15 +762,26 @@ function openAddress(template, values) {
|
|
|
573
762
|
missing = true;
|
|
574
763
|
return encodeURIComponent(String(value));
|
|
575
764
|
});
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
765
|
+
return missing ? null : webAddress(filled);
|
|
766
|
+
}
|
|
767
|
+
catch {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
/** `text` resolved against the page, where it is an `http:` or `https:` address. */
|
|
772
|
+
function webAddress(text) {
|
|
773
|
+
try {
|
|
774
|
+
const url = new URL(text, document.baseURI);
|
|
579
775
|
return url.protocol === 'http:' || url.protocol === 'https:' ? url.href : null;
|
|
580
776
|
}
|
|
581
777
|
catch {
|
|
582
778
|
return null;
|
|
583
779
|
}
|
|
584
780
|
}
|
|
781
|
+
/** The names in a list separated by spaces or commas. */
|
|
782
|
+
function listed(names) {
|
|
783
|
+
return names.split(/[\s,]+/).filter(Boolean);
|
|
784
|
+
}
|
|
585
785
|
/** A value as text, by the column's declared type. */
|
|
586
786
|
function present(value, column) {
|
|
587
787
|
if (value === null || value === undefined)
|
|
@@ -591,7 +791,7 @@ function present(value, column) {
|
|
|
591
791
|
if (typeof value === 'number')
|
|
592
792
|
return Number.isInteger(value) ? value.toLocaleString('en-GB') : String(value);
|
|
593
793
|
if (typeof value === 'boolean')
|
|
594
|
-
return value ? '
|
|
794
|
+
return BOOL_WORDS[value ? 'true' : 'false'];
|
|
595
795
|
return String(value);
|
|
596
796
|
}
|
|
597
797
|
attachContextRoot();
|