@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/explorer.js
CHANGED
|
@@ -37,9 +37,9 @@ import { css, html, nothing, svg } from 'lit';
|
|
|
37
37
|
import { property, state } from 'lit/decorators.js';
|
|
38
38
|
import { repeat } from 'lit/directives/repeat.js';
|
|
39
39
|
import { CATEGORY_PALETTES, DEFAULT_DENSITY_CELL_PX, DEFAULT_DENSITY_SCALE, DENSITY_CELL_SIZES, RAMPS, cellDepth, nearestStop } from '@mosaicajs/deck';
|
|
40
|
-
import { CLUSTER_PREFIX, PALETTES, WORLD_SIZE, activeCount, artifactName, colourLayers, emptyDraft } from '@mosaicajs/client';
|
|
41
|
-
import { hasOneLayout,
|
|
42
|
-
import { DENSITY_COLOUR_TITLES, colourOfFraction, css as rgb, hexOf } from '@mosaicajs/deck/internal';
|
|
40
|
+
import { CLUSTER_PREFIX, PALETTES, WORLD_SIZE, activeCount, artifactName, clustersInView, colourLayers, emptyDraft } from '@mosaicajs/client';
|
|
41
|
+
import { follows, hasOneLayout, levelFollowsZoom, levelOfLayer, sizesPoints } from '@mosaicajs/client/internal';
|
|
42
|
+
import { DENSITY_COLOUR_TITLES, clusterLayerOf, colourOfFraction, css as rgb, hexOf } from '@mosaicajs/deck/internal';
|
|
43
43
|
import { MosaicaElement, columnCaption, emit, shortCount } from './base.js';
|
|
44
44
|
import { placeCallout } from './callout.js';
|
|
45
45
|
import { colouringOf, setColouring, sizingOf, watchChoices } from './colouring.js';
|
|
@@ -110,6 +110,8 @@ const px = (n) => n.toLocaleString('en-GB', { maximumFractionDigits: 1 });
|
|
|
110
110
|
const fill = (v, min, max) => `--fill:${(Math.min(1, Math.max(0, (v - min) / (max - min))) * 100).toFixed(1)}%`;
|
|
111
111
|
/** A ramp as a CSS gradient, low to high. */
|
|
112
112
|
const rampGradient = (ramp, reverse) => `linear-gradient(to right, ${Array.from({ length: 8 }, (_, k) => rgb(colourOfFraction(k / 7, ramp, reverse))).join(', ')})`;
|
|
113
|
+
/** The tick on the entry chosen in a list of choices. */
|
|
114
|
+
const tick = (chosen) => (chosen ? html `<span class="tick">${icon('check', 11, 1.6)}</span>` : nothing);
|
|
113
115
|
/** The Density choices in the Layers popover, in order. */
|
|
114
116
|
const DENSITY_MODES = [
|
|
115
117
|
{ mode: 'none', label: 'None' },
|
|
@@ -124,7 +126,16 @@ const CALLOUT_SIZE = { width: 300, height: 220 };
|
|
|
124
126
|
const CALLOUT_REST_MS = 200;
|
|
125
127
|
/** The shortest a callout is made to fit beside its point; what does not fit scrolls inside it. */
|
|
126
128
|
const CALLOUT_MIN_HEIGHT = 180;
|
|
127
|
-
/** The
|
|
129
|
+
/** The controls Tab stops at. */
|
|
130
|
+
const FOCUSABLE = 'button:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
131
|
+
/** Whether `node` is `container` or inside it, through slots and shadow roots. */
|
|
132
|
+
function within(node, container) {
|
|
133
|
+
for (let at = node; at; at = (at instanceof Element ? at.assignedSlot : null) ?? at.parentNode ?? (at instanceof ShadowRoot ? at.host : null))
|
|
134
|
+
if (at === container)
|
|
135
|
+
return true;
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
/** The narrow layout's buttons, each opening a sheet, drawn where its panel is. */
|
|
128
139
|
const TABS = [
|
|
129
140
|
{ sheet: 'filters', icon: 'filter', label: 'Fields', panel: 'filters' },
|
|
130
141
|
{ sheet: 'layers', icon: 'layers', label: 'Layers', panel: 'legend' },
|
|
@@ -153,7 +164,20 @@ const TABS = [
|
|
|
153
164
|
* HTML.
|
|
154
165
|
*
|
|
155
166
|
* Below 1000 px the explorer uses the overlay layout whatever `layout` says. At 720 px or less it
|
|
156
|
-
* opens its panels as sheets from a
|
|
167
|
+
* opens its panels as sheets from a bar of buttons under the map, and a picked item's card opens
|
|
168
|
+
* in the Item sheet. A sheet that reaches up over the map's tools makes the map behind it inert
|
|
169
|
+
* until it closes.
|
|
170
|
+
*
|
|
171
|
+
* The status strip speaks for the explorer: its map and its selection card are set `quiet`, so a
|
|
172
|
+
* change is announced once. Where you put something other than a `<mosaica-status>` in the
|
|
173
|
+
* `status` slot, they announce for themselves.
|
|
174
|
+
*
|
|
175
|
+
* An `aria-label` on the explorer names its map, so a page with several explorers can tell them
|
|
176
|
+
* apart.
|
|
177
|
+
*
|
|
178
|
+
* A dataset title, a view's name or a layer's name too long for its line is cut with an ellipsis
|
|
179
|
+
* and keeps all of it in its `title`. Set `--mosaica-long-text: normal` to wrap them. The narrow
|
|
180
|
+
* layout wraps them itself.
|
|
157
181
|
*
|
|
158
182
|
* @summary A complete map application in a single element.
|
|
159
183
|
* @tagname mosaica-explorer
|
|
@@ -163,7 +187,7 @@ const TABS = [
|
|
|
163
187
|
* @slot filters - Replaces the filter panel.
|
|
164
188
|
* @slot selection - Replaces the selection panel, shown while a region is selected.
|
|
165
189
|
* @slot detail - Replaces the item and artifact cards, in the card beside the point.
|
|
166
|
-
* @slot status - Replaces the status strip
|
|
190
|
+
* @slot status - Replaces the status strip: on the map, or under it in the narrow layout.
|
|
167
191
|
* @slot tooltip - Replaces the map's hover tooltip.
|
|
168
192
|
* @slot top-right - Content in the map's top-right corner, above the selection's card.
|
|
169
193
|
* @fires {CustomEvent<MosaicaEventDetails['mosaica-viewchange']>} mosaica-viewchange - The map's camera moved.
|
|
@@ -176,11 +200,11 @@ const TABS = [
|
|
|
176
200
|
* @fires {CustomEvent<MosaicaEventDetails['mosaica-selectchange']>} mosaica-selectchange - A selection was drawn, changed or cleared, or its counts arrived.
|
|
177
201
|
* @fires {CustomEvent<MosaicaEventDetails['mosaica-layerchange']>} mosaica-layerchange - The layers chosen changed.
|
|
178
202
|
* @fires {CustomEvent<MosaicaEventDetails['mosaica-colourchange']>} mosaica-colourchange - Colour by changed, in the Layers popover or by a card's paint button.
|
|
179
|
-
* @fires {CustomEvent<MosaicaEventDetails['mosaica-levelchange']>} mosaica-levelchange - The level
|
|
203
|
+
* @fires {CustomEvent<MosaicaEventDetails['mosaica-levelchange']>} mosaica-levelchange - The level one layer is counted, coloured and labelled at changed.
|
|
180
204
|
* @fires {CustomEvent<MosaicaEventDetails['mosaica-displaychange']>} mosaica-displaychange - A setting in the Points or Density section changed.
|
|
181
205
|
* @fires {CustomEvent<MosaicaEventDetails['mosaica-sizechange']>} mosaica-sizechange - Size by, the size range or the scale changed in the Points section.
|
|
182
206
|
* @fires {CustomEvent<MosaicaEventDetails['mosaica-budgetchange']>} mosaica-budgetchange - Most points was let go at a new number.
|
|
183
|
-
* @fires {CustomEvent<MosaicaEventDetails['mosaica-
|
|
207
|
+
* @fires {CustomEvent<MosaicaEventDetails['mosaica-clusterspacingchange']>} mosaica-clusterspacingchange - Cluster density was let go at a new spacing.
|
|
184
208
|
* @fires {CustomEvent<MosaicaEventDetails['mosaica-fold']>} mosaica-fold - A field card was folded to one line or opened.
|
|
185
209
|
* @fires {CustomEvent<MosaicaEventDetails['mosaica-valuecolour']>} mosaica-valuecolour - Colours were chosen or reset for values of a category, on a field card or in Edit colours.
|
|
186
210
|
* @fires {CustomEvent<MosaicaEventDetails['mosaica-clustercolour']>} mosaica-clustercolour - Colours were chosen or reset for clusters of a layer, on a field card or in Edit colours.
|
|
@@ -217,8 +241,9 @@ const TABS = [
|
|
|
217
241
|
* @csspart points-toggle - The Points switch, with `aria-checked`.
|
|
218
242
|
* @csspart most-points - The Most points slider.
|
|
219
243
|
* @csspart most-points-value - The number of points it stands at, such as "250K".
|
|
220
|
-
* @csspart
|
|
221
|
-
*
|
|
244
|
+
* @csspart cluster-density - The Cluster density slider, while a `nested` or `dag` layer is drawn, or a
|
|
245
|
+
* levelled layer whose level follows the zoom is drawn or coloured by.
|
|
246
|
+
* @csspart cluster-density-value - How many clusters it gives the map at its size, such as "96".
|
|
222
247
|
* @csspart size-by - The Size by button, naming the column the points are sized by or None, with
|
|
223
248
|
* `aria-expanded` while its menu is open.
|
|
224
249
|
* @csspart size-menu - The Size by menu, while it is open.
|
|
@@ -235,7 +260,8 @@ const TABS = [
|
|
|
235
260
|
* `aria-expanded` while its menu is open.
|
|
236
261
|
* @csspart colour-menu - The Colour by menu, while it is open.
|
|
237
262
|
* @csspart colour-option - An entry in the Colour by menu, with `data-value` (empty for None,
|
|
238
|
-
* `cluster:<layer>@<level>` for one level of a levelled layer
|
|
263
|
+
* `cluster:<layer>@<level>` for one level of a levelled layer, `cluster:<layer>@` for Follow zoom
|
|
264
|
+
* on one whose levels declare no zoom range) and `aria-checked`.
|
|
239
265
|
* @csspart palette - The Palette button, while the points are coloured by a layer or a category.
|
|
240
266
|
* @csspart palette-menu - The Palette menu, while it is open.
|
|
241
267
|
* @csspart palette-option - A palette in that menu, with `data-value` and `aria-checked`.
|
|
@@ -259,9 +285,10 @@ const TABS = [
|
|
|
259
285
|
* @csspart detail - The item or artifact card in the map's top-right corner, used when the picked
|
|
260
286
|
* item or artifact has no position on the map, such as after following an item into another
|
|
261
287
|
* view.
|
|
262
|
-
* @csspart sheet - The open sheet, in the narrow layout.
|
|
288
|
+
* @csspart sheet - The open sheet, over the map in the narrow layout.
|
|
263
289
|
* @csspart strip-row - The full-width status strip, in the narrow layout.
|
|
264
|
-
* @csspart tabs - The
|
|
290
|
+
* @csspart tabs - The bar of buttons that open the sheets, in the narrow layout, each with
|
|
291
|
+
* `data-sheet` and `aria-expanded`.
|
|
265
292
|
* @csspart map-<part> - A part of the inner `<mosaica-map>`, such as `map-controls`.
|
|
266
293
|
* @csspart status-<part> - A part of an inner `<mosaica-status>`.
|
|
267
294
|
* @csspart view-picker-<part> - A part of the inner `<mosaica-view-picker>`.
|
|
@@ -305,6 +332,9 @@ let MosaicaExplorer = (() => {
|
|
|
305
332
|
let _cardFields_decorators;
|
|
306
333
|
let _cardFields_initializers = [];
|
|
307
334
|
let _cardFields_extraInitializers = [];
|
|
335
|
+
let _imageFields_decorators;
|
|
336
|
+
let _imageFields_initializers = [];
|
|
337
|
+
let _imageFields_extraInitializers = [];
|
|
308
338
|
let _openUrl_decorators;
|
|
309
339
|
let _openUrl_initializers = [];
|
|
310
340
|
let _openUrl_extraInitializers = [];
|
|
@@ -320,15 +350,15 @@ let MosaicaExplorer = (() => {
|
|
|
320
350
|
let _budgetMax_decorators;
|
|
321
351
|
let _budgetMax_initializers = [];
|
|
322
352
|
let _budgetMax_extraInitializers = [];
|
|
323
|
-
let
|
|
324
|
-
let
|
|
325
|
-
let
|
|
326
|
-
let
|
|
327
|
-
let
|
|
328
|
-
let
|
|
329
|
-
let
|
|
330
|
-
let
|
|
331
|
-
let
|
|
353
|
+
let _clusterSpacing_decorators;
|
|
354
|
+
let _clusterSpacing_initializers = [];
|
|
355
|
+
let _clusterSpacing_extraInitializers = [];
|
|
356
|
+
let _clusterSpacingMin_decorators;
|
|
357
|
+
let _clusterSpacingMin_initializers = [];
|
|
358
|
+
let _clusterSpacingMin_extraInitializers = [];
|
|
359
|
+
let _clusterSpacingMax_decorators;
|
|
360
|
+
let _clusterSpacingMax_initializers = [];
|
|
361
|
+
let _clusterSpacingMax_extraInitializers = [];
|
|
332
362
|
let _basemap_decorators;
|
|
333
363
|
let _basemap_initializers = [];
|
|
334
364
|
let _basemap_extraInitializers = [];
|
|
@@ -419,9 +449,9 @@ let MosaicaExplorer = (() => {
|
|
|
419
449
|
let _tabFocus_decorators;
|
|
420
450
|
let _tabFocus_initializers = [];
|
|
421
451
|
let _tabFocus_extraInitializers = [];
|
|
422
|
-
let
|
|
423
|
-
let
|
|
424
|
-
let
|
|
452
|
+
let _levels_decorators;
|
|
453
|
+
let _levels_initializers = [];
|
|
454
|
+
let _levels_extraInitializers = [];
|
|
425
455
|
let _pinned_decorators;
|
|
426
456
|
let _pinned_initializers = [];
|
|
427
457
|
let _pinned_extraInitializers = [];
|
|
@@ -448,14 +478,15 @@ let MosaicaExplorer = (() => {
|
|
|
448
478
|
_titleField_decorators = [property({ attribute: 'title-field' })];
|
|
449
479
|
_subtitleField_decorators = [property({ attribute: 'subtitle-field' })];
|
|
450
480
|
_cardFields_decorators = [property({ attribute: 'card-fields' })];
|
|
481
|
+
_imageFields_decorators = [property({ attribute: 'image-fields' })];
|
|
451
482
|
_openUrl_decorators = [property({ attribute: 'open-url' })];
|
|
452
483
|
_open_decorators = [property({ attribute: false })];
|
|
453
484
|
_budget_decorators = [property({ type: Number })];
|
|
454
485
|
_budgetMin_decorators = [property({ type: Number, attribute: 'budget-min' })];
|
|
455
486
|
_budgetMax_decorators = [property({ type: Number, attribute: 'budget-max' })];
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
487
|
+
_clusterSpacing_decorators = [property({ type: Number, attribute: 'cluster-spacing' })];
|
|
488
|
+
_clusterSpacingMin_decorators = [property({ type: Number, attribute: 'cluster-spacing-min' })];
|
|
489
|
+
_clusterSpacingMax_decorators = [property({ type: Number, attribute: 'cluster-spacing-max' })];
|
|
459
490
|
_basemap_decorators = [property({ hasChanged: basemapChanged })];
|
|
460
491
|
_noPoints_decorators = [property({ type: Boolean, attribute: 'no-points' })];
|
|
461
492
|
_radius_decorators = [property({ type: Number })];
|
|
@@ -486,7 +517,7 @@ let MosaicaExplorer = (() => {
|
|
|
486
517
|
_densityColoursOpen_decorators = [state()];
|
|
487
518
|
_sheet_decorators = [state()];
|
|
488
519
|
_tabFocus_decorators = [state()];
|
|
489
|
-
|
|
520
|
+
_levels_decorators = [state()];
|
|
490
521
|
_pinned_decorators = [state()];
|
|
491
522
|
_layersOpen_decorators = [state()];
|
|
492
523
|
_compact_decorators = [state()];
|
|
@@ -500,14 +531,15 @@ let MosaicaExplorer = (() => {
|
|
|
500
531
|
__esDecorate(this, null, _titleField_decorators, { kind: "accessor", name: "titleField", static: false, private: false, access: { has: obj => "titleField" in obj, get: obj => obj.titleField, set: (obj, value) => { obj.titleField = value; } }, metadata: _metadata }, _titleField_initializers, _titleField_extraInitializers);
|
|
501
532
|
__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);
|
|
502
533
|
__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);
|
|
534
|
+
__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);
|
|
503
535
|
__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);
|
|
504
536
|
__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);
|
|
505
537
|
__esDecorate(this, null, _budget_decorators, { kind: "accessor", name: "budget", static: false, private: false, access: { has: obj => "budget" in obj, get: obj => obj.budget, set: (obj, value) => { obj.budget = value; } }, metadata: _metadata }, _budget_initializers, _budget_extraInitializers);
|
|
506
538
|
__esDecorate(this, null, _budgetMin_decorators, { kind: "accessor", name: "budgetMin", static: false, private: false, access: { has: obj => "budgetMin" in obj, get: obj => obj.budgetMin, set: (obj, value) => { obj.budgetMin = value; } }, metadata: _metadata }, _budgetMin_initializers, _budgetMin_extraInitializers);
|
|
507
539
|
__esDecorate(this, null, _budgetMax_decorators, { kind: "accessor", name: "budgetMax", static: false, private: false, access: { has: obj => "budgetMax" in obj, get: obj => obj.budgetMax, set: (obj, value) => { obj.budgetMax = value; } }, metadata: _metadata }, _budgetMax_initializers, _budgetMax_extraInitializers);
|
|
508
|
-
__esDecorate(this, null,
|
|
509
|
-
__esDecorate(this, null,
|
|
510
|
-
__esDecorate(this, null,
|
|
540
|
+
__esDecorate(this, null, _clusterSpacing_decorators, { kind: "accessor", name: "clusterSpacing", static: false, private: false, access: { has: obj => "clusterSpacing" in obj, get: obj => obj.clusterSpacing, set: (obj, value) => { obj.clusterSpacing = value; } }, metadata: _metadata }, _clusterSpacing_initializers, _clusterSpacing_extraInitializers);
|
|
541
|
+
__esDecorate(this, null, _clusterSpacingMin_decorators, { kind: "accessor", name: "clusterSpacingMin", static: false, private: false, access: { has: obj => "clusterSpacingMin" in obj, get: obj => obj.clusterSpacingMin, set: (obj, value) => { obj.clusterSpacingMin = value; } }, metadata: _metadata }, _clusterSpacingMin_initializers, _clusterSpacingMin_extraInitializers);
|
|
542
|
+
__esDecorate(this, null, _clusterSpacingMax_decorators, { kind: "accessor", name: "clusterSpacingMax", static: false, private: false, access: { has: obj => "clusterSpacingMax" in obj, get: obj => obj.clusterSpacingMax, set: (obj, value) => { obj.clusterSpacingMax = value; } }, metadata: _metadata }, _clusterSpacingMax_initializers, _clusterSpacingMax_extraInitializers);
|
|
511
543
|
__esDecorate(this, null, _basemap_decorators, { kind: "accessor", name: "basemap", static: false, private: false, access: { has: obj => "basemap" in obj, get: obj => obj.basemap, set: (obj, value) => { obj.basemap = value; } }, metadata: _metadata }, _basemap_initializers, _basemap_extraInitializers);
|
|
512
544
|
__esDecorate(this, null, _noPoints_decorators, { kind: "accessor", name: "noPoints", static: false, private: false, access: { has: obj => "noPoints" in obj, get: obj => obj.noPoints, set: (obj, value) => { obj.noPoints = value; } }, metadata: _metadata }, _noPoints_initializers, _noPoints_extraInitializers);
|
|
513
545
|
__esDecorate(this, null, _radius_decorators, { kind: "accessor", name: "radius", static: false, private: false, access: { has: obj => "radius" in obj, get: obj => obj.radius, set: (obj, value) => { obj.radius = value; } }, metadata: _metadata }, _radius_initializers, _radius_extraInitializers);
|
|
@@ -538,7 +570,7 @@ let MosaicaExplorer = (() => {
|
|
|
538
570
|
__esDecorate(this, null, _densityColoursOpen_decorators, { kind: "accessor", name: "densityColoursOpen", static: false, private: false, access: { has: obj => "densityColoursOpen" in obj, get: obj => obj.densityColoursOpen, set: (obj, value) => { obj.densityColoursOpen = value; } }, metadata: _metadata }, _densityColoursOpen_initializers, _densityColoursOpen_extraInitializers);
|
|
539
571
|
__esDecorate(this, null, _sheet_decorators, { kind: "accessor", name: "sheet", static: false, private: false, access: { has: obj => "sheet" in obj, get: obj => obj.sheet, set: (obj, value) => { obj.sheet = value; } }, metadata: _metadata }, _sheet_initializers, _sheet_extraInitializers);
|
|
540
572
|
__esDecorate(this, null, _tabFocus_decorators, { kind: "accessor", name: "tabFocus", static: false, private: false, access: { has: obj => "tabFocus" in obj, get: obj => obj.tabFocus, set: (obj, value) => { obj.tabFocus = value; } }, metadata: _metadata }, _tabFocus_initializers, _tabFocus_extraInitializers);
|
|
541
|
-
__esDecorate(this, null,
|
|
573
|
+
__esDecorate(this, null, _levels_decorators, { kind: "accessor", name: "levels", static: false, private: false, access: { has: obj => "levels" in obj, get: obj => obj.levels, set: (obj, value) => { obj.levels = value; } }, metadata: _metadata }, _levels_initializers, _levels_extraInitializers);
|
|
542
574
|
__esDecorate(this, null, _pinned_decorators, { kind: "accessor", name: "pinned", static: false, private: false, access: { has: obj => "pinned" in obj, get: obj => obj.pinned, set: (obj, value) => { obj.pinned = value; } }, metadata: _metadata }, _pinned_initializers, _pinned_extraInitializers);
|
|
543
575
|
__esDecorate(this, null, _layersOpen_decorators, { kind: "accessor", name: "layersOpen", static: false, private: false, access: { has: obj => "layersOpen" in obj, get: obj => obj.layersOpen, set: (obj, value) => { obj.layersOpen = value; } }, metadata: _metadata }, _layersOpen_initializers, _layersOpen_extraInitializers);
|
|
544
576
|
__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);
|
|
@@ -593,6 +625,13 @@ let MosaicaExplorer = (() => {
|
|
|
593
625
|
height: 100%;
|
|
594
626
|
min-height: 320px;
|
|
595
627
|
}
|
|
628
|
+
/* The narrow layout's map takes the height its bars leave. */
|
|
629
|
+
@container explorer (max-width: 720px) {
|
|
630
|
+
mosaica-map,
|
|
631
|
+
::slotted(mosaica-map) {
|
|
632
|
+
min-height: 0;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
596
635
|
/* What the map draws at its top-left, its tools or a region's tag, sits right of the card; in
|
|
597
636
|
the narrow layout there is no card. */
|
|
598
637
|
@container explorer (width > 720px) {
|
|
@@ -689,11 +728,17 @@ let MosaicaExplorer = (() => {
|
|
|
689
728
|
min-width: 0;
|
|
690
729
|
}
|
|
691
730
|
.title {
|
|
731
|
+
display: block;
|
|
732
|
+
margin: 0;
|
|
692
733
|
font-size: 15px;
|
|
693
734
|
font-weight: 600;
|
|
735
|
+
letter-spacing: 0;
|
|
736
|
+
text-transform: none;
|
|
737
|
+
color: var(--_mosaica-ink);
|
|
694
738
|
overflow: hidden;
|
|
695
739
|
text-overflow: ellipsis;
|
|
696
|
-
white-space:
|
|
740
|
+
white-space: var(--_mosaica-long-text);
|
|
741
|
+
overflow-wrap: anywhere;
|
|
697
742
|
}
|
|
698
743
|
.compact .title {
|
|
699
744
|
font-size: 14px;
|
|
@@ -772,6 +817,10 @@ let MosaicaExplorer = (() => {
|
|
|
772
817
|
linear-gradient(transparent, color-mix(in srgb, var(--_mosaica-ink) 16%, transparent)) bottom / 100% 10px no-repeat scroll,
|
|
773
818
|
var(--_mosaica-surface);
|
|
774
819
|
}
|
|
820
|
+
[part='layers-popover']:focus-visible,
|
|
821
|
+
[part='sheet']:focus-visible {
|
|
822
|
+
outline-offset: -2px;
|
|
823
|
+
}
|
|
775
824
|
[part='layers-popover'] mosaica-layer-picker,
|
|
776
825
|
[part='sheet'] mosaica-layer-picker {
|
|
777
826
|
display: block;
|
|
@@ -852,6 +901,7 @@ let MosaicaExplorer = (() => {
|
|
|
852
901
|
}
|
|
853
902
|
[part='pin'][aria-pressed='true'] {
|
|
854
903
|
background: var(--_mosaica-surface-3);
|
|
904
|
+
box-shadow: inset 0 -2px 0 var(--_mosaica-accent);
|
|
855
905
|
color: var(--_mosaica-ink);
|
|
856
906
|
}
|
|
857
907
|
/* The selection keeps its heading, counts and actions; only its list of marks scrolls, so the
|
|
@@ -898,18 +948,22 @@ let MosaicaExplorer = (() => {
|
|
|
898
948
|
padding: 10px 16px;
|
|
899
949
|
}
|
|
900
950
|
@container explorer (max-width: 720px) {
|
|
951
|
+
/* The map keeps a floor under its height; a frame too short for it and the bars scrolls.
|
|
952
|
+
A long title or name wraps here, in the explorer and in the elements it holds. */
|
|
901
953
|
[part='frame'],
|
|
902
954
|
[part='frame'].docked {
|
|
955
|
+
--mosaica-long-text: normal;
|
|
956
|
+
--_mosaica-long-text: normal;
|
|
903
957
|
grid-template-columns: minmax(0, 1fr);
|
|
904
|
-
grid-template-rows: minmax(
|
|
958
|
+
grid-template-rows: minmax(120px, 1fr) auto auto;
|
|
959
|
+
overflow: hidden auto;
|
|
905
960
|
}
|
|
906
961
|
[part='frame'].titled {
|
|
907
|
-
grid-template-rows: auto minmax(
|
|
962
|
+
grid-template-rows: auto minmax(120px, 1fr) auto auto;
|
|
908
963
|
}
|
|
909
964
|
[part='sidebar'],
|
|
910
965
|
[part='panel'],
|
|
911
966
|
.layers,
|
|
912
|
-
.right,
|
|
913
967
|
.callouts,
|
|
914
968
|
.in-map-strip {
|
|
915
969
|
display: none;
|
|
@@ -933,30 +987,37 @@ let MosaicaExplorer = (() => {
|
|
|
933
987
|
font-size: 11px;
|
|
934
988
|
font-weight: 500;
|
|
935
989
|
}
|
|
936
|
-
[part='tabs'] button[aria-
|
|
990
|
+
[part='tabs'] button[aria-expanded='true'] {
|
|
991
|
+
box-shadow: inset 0 2px 0 var(--_mosaica-accent);
|
|
937
992
|
color: var(--_mosaica-ink);
|
|
938
993
|
font-weight: 600;
|
|
939
994
|
}
|
|
995
|
+
/* The sheet opens over the map alone, so the strip and the bar under it stay in reach. A
|
|
996
|
+
short map gives the sheet all of its height. */
|
|
940
997
|
[part='sheet'] {
|
|
941
998
|
position: absolute;
|
|
942
999
|
left: 0;
|
|
943
1000
|
right: 0;
|
|
944
|
-
bottom:
|
|
945
|
-
max-height:
|
|
946
|
-
|
|
1001
|
+
bottom: 0;
|
|
1002
|
+
max-height: max(80%, min(100%, 240px));
|
|
1003
|
+
display: flex;
|
|
1004
|
+
flex-direction: column;
|
|
1005
|
+
overflow: hidden;
|
|
947
1006
|
background: var(--_mosaica-surface);
|
|
948
1007
|
border-top: 1px solid var(--_mosaica-line);
|
|
949
1008
|
border-radius: 12px 12px 0 0;
|
|
950
1009
|
box-shadow: var(--_mosaica-shadow);
|
|
951
1010
|
z-index: 6;
|
|
952
1011
|
}
|
|
953
|
-
/*
|
|
954
|
-
|
|
955
|
-
|
|
1012
|
+
/* What the sheet holds scrolls between its handle and its footer, so nothing goes under the footer. */
|
|
1013
|
+
.sheet-body {
|
|
1014
|
+
flex: 0 1 auto;
|
|
1015
|
+
min-height: 0;
|
|
1016
|
+
overflow-y: auto;
|
|
1017
|
+
overscroll-behavior: contain;
|
|
956
1018
|
}
|
|
957
1019
|
.sheet-footer {
|
|
958
|
-
|
|
959
|
-
bottom: 0;
|
|
1020
|
+
flex: none;
|
|
960
1021
|
display: flex;
|
|
961
1022
|
gap: 10px;
|
|
962
1023
|
padding: 10px 16px;
|
|
@@ -973,7 +1034,7 @@ let MosaicaExplorer = (() => {
|
|
|
973
1034
|
}
|
|
974
1035
|
[part='sheet']::before {
|
|
975
1036
|
content: '';
|
|
976
|
-
|
|
1037
|
+
flex: none;
|
|
977
1038
|
width: 36px;
|
|
978
1039
|
height: 4px;
|
|
979
1040
|
border-radius: 2px;
|
|
@@ -992,8 +1053,18 @@ let MosaicaExplorer = (() => {
|
|
|
992
1053
|
box-shadow: none;
|
|
993
1054
|
border-radius: 0;
|
|
994
1055
|
border-width: 1px 0 0;
|
|
995
|
-
height: 44px;
|
|
996
|
-
|
|
1056
|
+
min-height: 44px;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
@media (forced-colors: active) {
|
|
1060
|
+
[part='layers-toggle'] .on {
|
|
1061
|
+
forced-color-adjust: none;
|
|
1062
|
+
background: Highlight;
|
|
1063
|
+
}
|
|
1064
|
+
[part='tabs'] button[aria-expanded='true'] {
|
|
1065
|
+
forced-color-adjust: none;
|
|
1066
|
+
background: Highlight;
|
|
1067
|
+
color: HighlightText;
|
|
997
1068
|
}
|
|
998
1069
|
}
|
|
999
1070
|
`
|
|
@@ -1038,7 +1109,11 @@ let MosaicaExplorer = (() => {
|
|
|
1038
1109
|
/** The fields the item card shows before "Show all", in order, as for `<mosaica-item-card>`. Unset, the first three. */
|
|
1039
1110
|
get cardFields() { return this.#cardFields_accessor_storage; }
|
|
1040
1111
|
set cardFields(value) { this.#cardFields_accessor_storage = value; }
|
|
1041
|
-
#
|
|
1112
|
+
#imageFields_accessor_storage = (__runInitializers(this, _cardFields_extraInitializers), __runInitializers(this, _imageFields_initializers, ''));
|
|
1113
|
+
/** The fields the item card draws as images, as for `<mosaica-item-card>`'s `image-fields`. */
|
|
1114
|
+
get imageFields() { return this.#imageFields_accessor_storage; }
|
|
1115
|
+
set imageFields(value) { this.#imageFields_accessor_storage = value; }
|
|
1116
|
+
#openUrl_accessor_storage = (__runInitializers(this, _imageFields_extraInitializers), __runInitializers(this, _openUrl_initializers, ''));
|
|
1042
1117
|
/** The link template for the item card's Open button, as for `<mosaica-item-card>`. Without it or `open`, there is no Open button. */
|
|
1043
1118
|
get openUrl() { return this.#openUrl_accessor_storage; }
|
|
1044
1119
|
set openUrl(value) { this.#openUrl_accessor_storage = value; }
|
|
@@ -1061,22 +1136,24 @@ let MosaicaExplorer = (() => {
|
|
|
1061
1136
|
/** The highest value of the Most points slider. */
|
|
1062
1137
|
get budgetMax() { return this.#budgetMax_accessor_storage; }
|
|
1063
1138
|
set budgetMax(value) { this.#budgetMax_accessor_storage = value; }
|
|
1064
|
-
#
|
|
1139
|
+
#clusterSpacing_accessor_storage = (__runInitializers(this, _budgetMax_extraInitializers), __runInitializers(this, _clusterSpacing_initializers, 100));
|
|
1065
1140
|
/**
|
|
1066
|
-
* The
|
|
1067
|
-
*
|
|
1141
|
+
* The side in CSS pixels of the square of the map each cluster is given, as for `<mosaica-map>`'s
|
|
1142
|
+
* `cluster-spacing`: at 100, a map 1,200 by 800 pixels cuts a `nested` or `dag` layer to 96
|
|
1143
|
+
* clusters, and a level that follows the zoom may have 96 in view. `0` removes the limit. Most
|
|
1144
|
+
* clusters in the Layers popover changes it.
|
|
1068
1145
|
*/
|
|
1069
|
-
get
|
|
1070
|
-
set
|
|
1071
|
-
#
|
|
1072
|
-
/** The
|
|
1073
|
-
get
|
|
1074
|
-
set
|
|
1075
|
-
#
|
|
1076
|
-
/** The
|
|
1077
|
-
get
|
|
1078
|
-
set
|
|
1079
|
-
#basemap_accessor_storage = (__runInitializers(this,
|
|
1146
|
+
get clusterSpacing() { return this.#clusterSpacing_accessor_storage; }
|
|
1147
|
+
set clusterSpacing(value) { this.#clusterSpacing_accessor_storage = value; }
|
|
1148
|
+
#clusterSpacingMin_accessor_storage = (__runInitializers(this, _clusterSpacing_extraInitializers), __runInitializers(this, _clusterSpacingMin_initializers, 50));
|
|
1149
|
+
/** The smallest spacing Cluster density offers, at its More end. */
|
|
1150
|
+
get clusterSpacingMin() { return this.#clusterSpacingMin_accessor_storage; }
|
|
1151
|
+
set clusterSpacingMin(value) { this.#clusterSpacingMin_accessor_storage = value; }
|
|
1152
|
+
#clusterSpacingMax_accessor_storage = (__runInitializers(this, _clusterSpacingMin_extraInitializers), __runInitializers(this, _clusterSpacingMax_initializers, 400));
|
|
1153
|
+
/** The largest spacing Cluster density offers, at its Fewer end. */
|
|
1154
|
+
get clusterSpacingMax() { return this.#clusterSpacingMax_accessor_storage; }
|
|
1155
|
+
set clusterSpacingMax(value) { this.#clusterSpacingMax_accessor_storage = value; }
|
|
1156
|
+
#basemap_accessor_storage = (__runInitializers(this, _clusterSpacingMax_extraInitializers), __runInitializers(this, _basemap_initializers, null));
|
|
1080
1157
|
/**
|
|
1081
1158
|
* What is drawn under the points, as for `<mosaica-map>`'s `basemap`. The attribute takes `world`
|
|
1082
1159
|
* and `none`. Unset, a view with a map projection gets the world outline and any other view gets
|
|
@@ -1191,7 +1268,7 @@ let MosaicaExplorer = (() => {
|
|
|
1191
1268
|
get pointsDragged() { return this.#pointsDragged_accessor_storage; }
|
|
1192
1269
|
set pointsDragged(value) { this.#pointsDragged_accessor_storage = value; }
|
|
1193
1270
|
#clustersDragged_accessor_storage = (__runInitializers(this, _pointsDragged_extraInitializers), __runInitializers(this, _clustersDragged_initializers, null));
|
|
1194
|
-
/**
|
|
1271
|
+
/** The cluster spacing Cluster density is being dragged to, before it is let go. @internal */
|
|
1195
1272
|
get clustersDragged() { return this.#clustersDragged_accessor_storage; }
|
|
1196
1273
|
set clustersDragged(value) { this.#clustersDragged_accessor_storage = value; }
|
|
1197
1274
|
#densityColoursOpen_accessor_storage = (__runInitializers(this, _clustersDragged_extraInitializers), __runInitializers(this, _densityColoursOpen_initializers, false));
|
|
@@ -1203,14 +1280,33 @@ let MosaicaExplorer = (() => {
|
|
|
1203
1280
|
get sheet() { return this.#sheet_accessor_storage; }
|
|
1204
1281
|
set sheet(value) { this.#sheet_accessor_storage = value; }
|
|
1205
1282
|
#tabFocus_accessor_storage = (__runInitializers(this, _sheet_extraInitializers), __runInitializers(this, _tabFocus_initializers, null));
|
|
1206
|
-
/** The narrow layout's
|
|
1283
|
+
/** The narrow layout's button focused last, which keeps the bar's one place in the tab order. */
|
|
1207
1284
|
get tabFocus() { return this.#tabFocus_accessor_storage; }
|
|
1208
1285
|
set tabFocus(value) { this.#tabFocus_accessor_storage = value; }
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1286
|
+
/**
|
|
1287
|
+
* What the selection's card shows, as last seen: the record or the refusal the store holds, or
|
|
1288
|
+
* the map's broken pick; null for no card. Picking the same item again brings a new record.
|
|
1289
|
+
*/
|
|
1290
|
+
cardSeen = (__runInitializers(this, _tabFocus_extraInitializers), null);
|
|
1291
|
+
/** Where focus was before the update being drawn: in the open sheet, elsewhere in the explorer, or outside it. */
|
|
1292
|
+
focusBefore = null;
|
|
1293
|
+
/** Whether the host put something other than a status strip in the `status` slot, so no strip speaks. */
|
|
1294
|
+
get stripReplaced() {
|
|
1295
|
+
const given = Array.from(this.children).filter((el) => el.slot === 'status');
|
|
1296
|
+
return given.length > 0 && !given.some((el) => el.localName === 'mosaica-status' || el.querySelector('mosaica-status') !== null);
|
|
1297
|
+
}
|
|
1298
|
+
/** What the host put in the `status` slot changed, and with it whether a strip speaks. */
|
|
1299
|
+
onStatusSlot = () => this.requestUpdate();
|
|
1300
|
+
/** What the status region says: the card that opened last. */
|
|
1301
|
+
announced = '';
|
|
1302
|
+
#levels_accessor_storage = __runInitializers(this, _levels_initializers, new Map());
|
|
1303
|
+
/**
|
|
1304
|
+
* The level each layer is counted, coloured and labelled at, chosen on its card or in Colour by,
|
|
1305
|
+
* keyed by layer name. A layer with no entry takes its default level. @internal
|
|
1306
|
+
*/
|
|
1307
|
+
get levels() { return this.#levels_accessor_storage; }
|
|
1308
|
+
set levels(value) { this.#levels_accessor_storage = value; }
|
|
1309
|
+
#pinned_accessor_storage = (__runInitializers(this, _levels_extraInitializers), __runInitializers(this, _pinned_initializers, []));
|
|
1214
1310
|
/** The cards kept open by Pin, oldest first. @internal */
|
|
1215
1311
|
get pinned() { return this.#pinned_accessor_storage; }
|
|
1216
1312
|
set pinned(value) { this.#pinned_accessor_storage = value; }
|
|
@@ -1285,6 +1381,61 @@ let MosaicaExplorer = (() => {
|
|
|
1285
1381
|
}
|
|
1286
1382
|
super.onStoreChange();
|
|
1287
1383
|
}
|
|
1384
|
+
static get observedAttributes() {
|
|
1385
|
+
return [...super.observedAttributes, 'aria-label'];
|
|
1386
|
+
}
|
|
1387
|
+
/** An `aria-label` on the explorer is passed to its map. */
|
|
1388
|
+
attributeChangedCallback(name, old, value) {
|
|
1389
|
+
super.attributeChangedCallback(name, old, value);
|
|
1390
|
+
if (name === 'aria-label')
|
|
1391
|
+
this.requestUpdate();
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* As the selection's card comes, changes or goes. The wide layouts say which card opened in the
|
|
1395
|
+
* status region. The narrow layout opens the Item sheet for it, and closes that sheet with it; a
|
|
1396
|
+
* card that arrives while the viewer's focus is in another sheet leaves that sheet open. Sheets
|
|
1397
|
+
* are the narrow layout's: growing wider closes the open one, and growing narrow with a card
|
|
1398
|
+
* open opens the Item sheet.
|
|
1399
|
+
*/
|
|
1400
|
+
willUpdate(changed) {
|
|
1401
|
+
super.willUpdate(changed);
|
|
1402
|
+
const sheet = this.renderRoot?.querySelector('[part="sheet"]') ?? null;
|
|
1403
|
+
this.focusBefore = sheet && within(this.shadowRoot?.activeElement ?? null, sheet) ? 'sheet' : this.holdsFocus() ? 'explorer' : null;
|
|
1404
|
+
const card = this.cardSubject();
|
|
1405
|
+
const fresh = card !== this.cardSeen;
|
|
1406
|
+
this.cardSeen = card;
|
|
1407
|
+
if (fresh)
|
|
1408
|
+
this.announced = card && !this.narrow ? `${this.cardName(this.liveKey())}, card opened` : '';
|
|
1409
|
+
if (!this.narrow) {
|
|
1410
|
+
this.sheet = null;
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
if (card && (fresh || changed.has('narrow'))) {
|
|
1414
|
+
if (this.sheet === null || this.focusBefore === 'explorer')
|
|
1415
|
+
this.sheet = 'detail';
|
|
1416
|
+
}
|
|
1417
|
+
else if (fresh && this.sheet === 'detail')
|
|
1418
|
+
this.sheet = null;
|
|
1419
|
+
}
|
|
1420
|
+
/** Whether focus is in the explorer: in its own tree, or in an element the host put in a slot. */
|
|
1421
|
+
holdsFocus() {
|
|
1422
|
+
const active = this.getRootNode().activeElement;
|
|
1423
|
+
return active !== null && this.contains(active);
|
|
1424
|
+
}
|
|
1425
|
+
/** What the selection's card shows, or null where it has nothing to show or `panels` leaves the card out. */
|
|
1426
|
+
cardSubject() {
|
|
1427
|
+
if (!this.has('detail'))
|
|
1428
|
+
return null;
|
|
1429
|
+
const sel = this.resolvedStore?.get('selection');
|
|
1430
|
+
const broken = this.map?.lastPick?.kind === 'broken' ? this.map.lastPick : null;
|
|
1431
|
+
const artifact = sel?.artifact ?? sel?.artifactRefusal ?? null;
|
|
1432
|
+
return (this.showsArtifact() ? artifact : (sel?.item ?? sel?.itemRefusal ?? artifact)) ?? broken;
|
|
1433
|
+
}
|
|
1434
|
+
/** Whether the selection has a card to show: a click that found nothing shows none, and a broken pick shows its fault. */
|
|
1435
|
+
hasDetail() {
|
|
1436
|
+
const selection = this.resolvedStore?.get('selection');
|
|
1437
|
+
return Boolean(selection?.item || selection?.artifact || selection?.artifactRefusal || selection?.itemRefusal || this.map?.lastPick?.kind === 'broken');
|
|
1438
|
+
}
|
|
1288
1439
|
connectedCallback() {
|
|
1289
1440
|
super.connectedCallback();
|
|
1290
1441
|
if (typeof ResizeObserver === 'undefined')
|
|
@@ -1341,6 +1492,19 @@ let MosaicaExplorer = (() => {
|
|
|
1341
1492
|
if (group && !path.includes(group) && !(editor && path.includes(editor)))
|
|
1342
1493
|
this.closeLayers();
|
|
1343
1494
|
};
|
|
1495
|
+
/**
|
|
1496
|
+
* Focus going from the Layers button, its popover or their menus to somewhere else in the page
|
|
1497
|
+
* closes the popover. The Edit colours dialog it opens is not somewhere else.
|
|
1498
|
+
*/
|
|
1499
|
+
onLayersFocusOut = (e) => {
|
|
1500
|
+
const to = e.relatedTarget;
|
|
1501
|
+
if (!this.layersOpen || !(to instanceof Node))
|
|
1502
|
+
return;
|
|
1503
|
+
const editor = this.renderRoot.querySelector('mosaica-colour-editor');
|
|
1504
|
+
if (within(to, e.currentTarget) || (editor && within(to, editor)))
|
|
1505
|
+
return;
|
|
1506
|
+
this.closeLayers();
|
|
1507
|
+
};
|
|
1344
1508
|
openLayers() {
|
|
1345
1509
|
this.layersOpen = true;
|
|
1346
1510
|
document.addEventListener('pointerdown', this.onOutside, true);
|
|
@@ -1348,12 +1512,12 @@ let MosaicaExplorer = (() => {
|
|
|
1348
1512
|
closeLayers() {
|
|
1349
1513
|
this.layersOpen = false;
|
|
1350
1514
|
this.menu = null;
|
|
1515
|
+
this.densityColoursOpen = false;
|
|
1351
1516
|
document.removeEventListener('pointerdown', this.onOutside, true);
|
|
1352
1517
|
}
|
|
1353
1518
|
render() {
|
|
1354
1519
|
const s = this.resolvedStore;
|
|
1355
1520
|
const region = s?.get('region') ?? null;
|
|
1356
|
-
const selection = s?.get('selection');
|
|
1357
1521
|
const meta = s?.get('meta') ?? null;
|
|
1358
1522
|
const artifacts = s?.get('artifacts');
|
|
1359
1523
|
const layersOn = artifacts?.layers.length ?? 0;
|
|
@@ -1362,9 +1526,8 @@ let MosaicaExplorer = (() => {
|
|
|
1362
1526
|
const narrow = this.narrow;
|
|
1363
1527
|
// Short of room, the docked layout takes the overlay's form.
|
|
1364
1528
|
const floating = this.layout === 'overlay' || compact;
|
|
1365
|
-
const
|
|
1366
|
-
|
|
1367
|
-
const hasDetail = Boolean(selection?.item || selection?.artifact || selection?.artifactRefusal || selection?.itemRefusal || this.map?.lastPick?.kind === 'broken');
|
|
1529
|
+
const levels = this.levels;
|
|
1530
|
+
const hasDetail = this.hasDetail();
|
|
1368
1531
|
// The detail shows whichever changed last.
|
|
1369
1532
|
const showArtifact = this.showsArtifact();
|
|
1370
1533
|
const detail = this.liveCard(showArtifact);
|
|
@@ -1377,43 +1540,50 @@ let MosaicaExplorer = (() => {
|
|
|
1377
1540
|
const shown = meta?.views.find((v) => v.id === s?.get('view').id) ?? (meta?.views.length === 1 ? meta.views[0] : undefined);
|
|
1378
1541
|
// A name that is only the view's id says nothing to a reader.
|
|
1379
1542
|
const viewName = shown && shown.displayName !== shown.id ? shown.displayName : '';
|
|
1380
|
-
|
|
1543
|
+
// The heading is the dataset's title, or without one the view's name.
|
|
1381
1544
|
const names = this.datasetTitle
|
|
1382
|
-
? html `<div class="names"><
|
|
1383
|
-
: html `<div class="names">${pickers}${
|
|
1545
|
+
? html `<div class="names"><h2 part="dataset-title" class="title" title=${this.datasetTitle}>${this.datasetTitle}</h2><div class="sub">${pickers}${pickersShown || !viewName ? nothing : html `<span part="view-name">${viewName}</span>`}</div></div>`
|
|
1546
|
+
: html `<div class="names">${pickers}${pickersShown || !viewName ? nothing : html `<h2 part="view-name" class="title" title=${viewName}>${viewName}</h2>`}</div>`;
|
|
1384
1547
|
const named = pickersShown || viewName !== '' || this.datasetTitle !== '';
|
|
1385
1548
|
const layersPanel = html `<slot name="layers"><mosaica-layer-picker exportparts=${FORWARD['layer-picker']}></mosaica-layer-picker></slot>`;
|
|
1386
|
-
const filters = html `<slot name="filters"><mosaica-filter-panel exportparts=${FORWARD['filter-panel']} pinned=${this.pinnedFilters || nothing} ?compact=${compact || narrow} .
|
|
1387
|
-
|
|
1549
|
+
const filters = html `<slot name="filters"><mosaica-filter-panel exportparts=${FORWARD['filter-panel']} pinned=${this.pinnedFilters || nothing} ?compact=${compact || narrow} .clusterLevels=${levels}></mosaica-filter-panel></slot>`;
|
|
1550
|
+
// The status strip speaks for the map and the selection card, unless the host replaced it with something else.
|
|
1551
|
+
const quiet = !this.stripReplaced;
|
|
1552
|
+
const selectionPanel = this.has('selection') && region ? html `<slot name="selection"><mosaica-selection exportparts=${FORWARD.selection} ?quiet=${quiet}></mosaica-selection></slot>` : nothing;
|
|
1388
1553
|
const selectionCard = selectionPanel === nothing ? nothing : html `<div part="selection-card" class="card">${selectionPanel}</div>`;
|
|
1389
1554
|
const head = html `<div class="head" ?hidden=${!named}>${names}</div>`;
|
|
1390
1555
|
// The card beside the point, where the map holds a position for what is selected; else the
|
|
1391
1556
|
// card stands in the top-right corner.
|
|
1392
1557
|
const anchor = this.liveAnchor(showArtifact);
|
|
1393
1558
|
const detailInColumn = this.has('detail') && hasDetail && anchor === null;
|
|
1394
|
-
|
|
1395
|
-
const
|
|
1559
|
+
// The narrow layout shows the selection and the detail in the Item sheet.
|
|
1560
|
+
const right = html `<div slot="top-right" class="right"><slot name="top-right"></slot>${narrow ? nothing : selectionCard}${detailInColumn && !narrow ? html `<div part="detail" class="card">${detail}</div>` : nothing}</div>`;
|
|
1561
|
+
// The column is named for what it holds.
|
|
1562
|
+
const columnName = this.has('filters') ? 'Fields' : 'View';
|
|
1563
|
+
const docked = html `<aside part="sidebar" aria-label=${columnName}>
|
|
1396
1564
|
${this.has('toolbar') ? head : nothing}
|
|
1397
1565
|
${this.has('filters') ? filters : nothing}
|
|
1398
1566
|
</aside>`;
|
|
1399
1567
|
const panel = this.has('toolbar') || this.has('filters')
|
|
1400
|
-
? html `<div part="panel" class="card" role="region" aria-label
|
|
1568
|
+
? html `<div part="panel" class="card" role="region" aria-label=${columnName}>${this.has('toolbar') ? head : nothing}${this.has('filters') ? filters : nothing}</div>`
|
|
1401
1569
|
: nothing;
|
|
1402
1570
|
const layersButton = this.has('legend')
|
|
1403
|
-
? html `<div class="layers" slot=${compact ? 'bottom-left' : 'top-left'} @keydown=${this.onLayersKey}>
|
|
1571
|
+
? html `<div class="layers" slot=${compact ? 'bottom-left' : 'top-left'} @keydown=${this.onLayersKey} @focusout=${this.onLayersFocusOut}>
|
|
1404
1572
|
<div class="group"><button part="layers-toggle" type="button" aria-label=${`Layers and display, ${layersOn} layers on`} aria-expanded=${this.layersOpen ? 'true' : 'false'}
|
|
1405
|
-
aria-controls
|
|
1573
|
+
aria-controls=${this.layersOpen ? 'layers-popover' : nothing} ?data-on=${layersOn > 0} @click=${() => (this.layersOpen ? this.closeLayers() : this.openLayers())}>${icon('layers', 16, 1.2)}${layersOn > 0 ? html `<span class="on"></span>` : nothing}</button></div>
|
|
1406
1574
|
${this.layersOpen
|
|
1407
|
-
? html `<div part="layers-popover" id="layers-popover" class="card" role="dialog" aria-label="Layers and display" @pointerdown=${this.onPopoverPress}>${this.displaySection()}${layersPanel}</div>${this.choiceMenu()}`
|
|
1575
|
+
? html `<div part="layers-popover" id="layers-popover" class="card" role="dialog" aria-label="Layers and display" tabindex="-1" @pointerdown=${this.onPopoverPress}>${this.displaySection()}${layersPanel}</div>${this.choiceMenu()}`
|
|
1408
1576
|
: nothing}
|
|
1409
1577
|
</div>`
|
|
1410
1578
|
: nothing;
|
|
1411
1579
|
const tabs = TABS.filter((t) => this.has(t.panel));
|
|
1412
|
-
// One
|
|
1580
|
+
// One button is in the page's tab order: the one focused last, else the open sheet's, else the first.
|
|
1413
1581
|
const reachable = tabs.find((t) => t.sheet === this.tabFocus) ?? tabs.find((t) => t.sheet === this.sheet) ?? tabs[0];
|
|
1414
|
-
const tab = ({ sheet, icon: ic, label }) => html `<button type="button"
|
|
1415
|
-
aria-
|
|
1416
|
-
@click=${() => {
|
|
1582
|
+
const tab = ({ sheet, icon: ic, label }) => html `<button type="button" id=${`opens-${sheet}`} data-sheet=${sheet} tabindex=${reachable?.sheet === sheet ? '0' : '-1'}
|
|
1583
|
+
aria-expanded=${this.sheet === sheet ? 'true' : 'false'} aria-controls=${this.sheet === sheet ? 'sheet' : nothing}
|
|
1584
|
+
@click=${(e) => {
|
|
1585
|
+
// A press does not focus a button in every browser, and the sheet takes the focus the explorer holds.
|
|
1586
|
+
e.currentTarget.focus();
|
|
1417
1587
|
this.tabFocus = sheet;
|
|
1418
1588
|
this.sheet = this.sheet === sheet ? null : sheet;
|
|
1419
1589
|
}}>${icon(ic, 18)}${label}</button>`;
|
|
@@ -1433,16 +1603,16 @@ let MosaicaExplorer = (() => {
|
|
|
1433
1603
|
<button class="btn primary" type="button" @click=${() => (this.sheet = null)}>${matchedText}</button>
|
|
1434
1604
|
</div>`;
|
|
1435
1605
|
const sheetBody = this.sheet === 'filters'
|
|
1436
|
-
? html
|
|
1606
|
+
? html `<div class="sheet-body">${filters}</div>${sheetFooter}`
|
|
1437
1607
|
: this.sheet === 'layers'
|
|
1438
|
-
? html
|
|
1608
|
+
? html `<div class="sheet-body">${this.displaySection()}${layersPanel}${this.choiceMenu()}</div>`
|
|
1439
1609
|
: this.sheet === 'detail'
|
|
1440
|
-
? html
|
|
1610
|
+
? html `<div class="sheet-body">${selectionPanel}${detail}</div>`
|
|
1441
1611
|
: nothing;
|
|
1442
1612
|
// The tooltip slot is forwarded only when the host supplied one: a slot assigned another slot
|
|
1443
1613
|
// counts as filled even when that slot is empty, which would hide the map's own tooltip.
|
|
1444
1614
|
return html `<div part="frame" class=${`${floating ? 'floating' : 'docked'}${compact ? ' compact' : ''}${narrow && this.has('toolbar') && named ? ' titled' : ''}`}
|
|
1445
|
-
@mosaica-levelchange=${(e) =>
|
|
1615
|
+
@mosaica-levelchange=${(e) => this.chooseLevel(e.detail.layer, e.detail.level)}
|
|
1446
1616
|
@mosaica-artifactfit=${(e) => this.map?.fitTo(BigInt(e.detail.id))}
|
|
1447
1617
|
@mosaica-viewfollow=${(e) => this.followItem(e.detail)}
|
|
1448
1618
|
@mosaica-close=${(e) => this.closeDetail(e)}>
|
|
@@ -1452,14 +1622,16 @@ let MosaicaExplorer = (() => {
|
|
|
1452
1622
|
${floating && !narrow ? panel : nothing}
|
|
1453
1623
|
<mosaica-map
|
|
1454
1624
|
exportparts=${FORWARD.map}
|
|
1625
|
+
aria-label=${this.getAttribute('aria-label') ?? nothing}
|
|
1626
|
+
?quiet=${quiet}
|
|
1455
1627
|
colour-by=${this.colourBy || nothing}
|
|
1456
1628
|
layers=${this.layers || nothing}
|
|
1457
1629
|
tooltip-fields=${this.tooltipFields}
|
|
1458
1630
|
title-field=${this.titleField || nothing}
|
|
1459
1631
|
budget=${this.budget || nothing}
|
|
1460
|
-
.
|
|
1632
|
+
.clusterSpacing=${this.clusterSpacing}
|
|
1461
1633
|
controls-corner=${compact ? 'bottom-left' : 'top-left'}
|
|
1462
|
-
.
|
|
1634
|
+
.clusterLevels=${levels}
|
|
1463
1635
|
.basemap=${this.basemap}
|
|
1464
1636
|
.noPoints=${this.noPoints}
|
|
1465
1637
|
.radius=${this.radius}
|
|
@@ -1484,22 +1656,24 @@ let MosaicaExplorer = (() => {
|
|
|
1484
1656
|
@mosaica-pick=${() => this.requestUpdate()}
|
|
1485
1657
|
@mosaica-itemopen=${() => this.requestUpdate()}
|
|
1486
1658
|
@mosaica-miss=${() => this.onMiss()}
|
|
1487
|
-
@keydown=${this.onMapKey}
|
|
1488
1659
|
@click=${() => this.requestUpdate()}
|
|
1489
1660
|
>
|
|
1490
1661
|
${narrow ? nothing : layersButton}
|
|
1491
|
-
${
|
|
1492
|
-
${narrow ? nothing : html `<div slot="bottom-right" class="in-map-strip"><slot name="status"><mosaica-status exportparts=${FORWARD.status} ?compact=${compact}></mosaica-status></slot></div>`}
|
|
1662
|
+
${right}
|
|
1663
|
+
${narrow ? nothing : html `<div slot="bottom-right" class="in-map-strip"><slot name="status" @slotchange=${this.onStatusSlot}><mosaica-status exportparts=${FORWARD.status} ?compact=${compact}></mosaica-status></slot></div>`}
|
|
1493
1664
|
${this.querySelector('[slot="tooltip"]') ? html `<slot name="tooltip" slot="tooltip"></slot>` : nothing}
|
|
1494
1665
|
</mosaica-map>
|
|
1495
1666
|
${narrow ? nothing : this.callouts(this.has('detail') && hasDetail ? anchor : null, detail)}
|
|
1667
|
+
${narrow && this.sheet && sheetBody !== nothing
|
|
1668
|
+
? html `<div part="sheet" id="sheet" role="dialog" aria-labelledby=${`opens-${this.sheet}`} tabindex="-1" @keydown=${this.onSheetKey}>${sheetBody}</div>`
|
|
1669
|
+
: nothing}
|
|
1496
1670
|
</div>
|
|
1497
|
-
|
|
1498
|
-
|
|
1671
|
+
<div class="sr-only" role="status">${this.announced}</div>
|
|
1672
|
+
${narrow
|
|
1673
|
+
? html `<div part="strip-row"><slot name="status" @slotchange=${this.onStatusSlot}><mosaica-status exportparts=${FORWARD.status}></mosaica-status></slot></div>
|
|
1674
|
+
<div part="tabs" role="toolbar" aria-label="Panels" @keydown=${this.onTabKey}>${tabs.map(tab)}</div>`
|
|
1499
1675
|
: nothing}
|
|
1500
|
-
<
|
|
1501
|
-
<div part="tabs" role="tablist" aria-label="Explorer panels" @keydown=${this.onTabKey}>${tabs.map(tab)}</div>
|
|
1502
|
-
<mosaica-colour-editor exportparts=${FORWARD['colour-editor']} field=${s?.get('legend').colourBy ?? ''} .level=${level}></mosaica-colour-editor>
|
|
1676
|
+
<mosaica-colour-editor exportparts=${FORWARD['colour-editor']} field=${s?.get('legend').colourBy ?? ''} .clusterLevels=${levels}></mosaica-colour-editor>
|
|
1503
1677
|
</div>`;
|
|
1504
1678
|
}
|
|
1505
1679
|
/** The item or artifact card for what is selected, or the host's in the `detail` slot. */
|
|
@@ -1514,7 +1688,7 @@ let MosaicaExplorer = (() => {
|
|
|
1514
1688
|
*/
|
|
1515
1689
|
itemCard(item, actions) {
|
|
1516
1690
|
return html `<mosaica-item-card exportparts=${FORWARD['item-card']} compact .item=${item} .pick=${item ? null : (this.map?.lastPick ?? null)}
|
|
1517
|
-
title-field=${this.titleField || nothing} subtitle-field=${this.subtitleField || nothing} card-fields=${this.cardFields || nothing} open-url=${this.openUrl || nothing} .open=${this.open}>${actions}</mosaica-item-card>`;
|
|
1691
|
+
title-field=${this.titleField || nothing} subtitle-field=${this.subtitleField || nothing} card-fields=${this.cardFields || nothing} image-fields=${this.imageFields || nothing} open-url=${this.openUrl || nothing} .open=${this.open}>${actions}</mosaica-item-card>`;
|
|
1518
1692
|
}
|
|
1519
1693
|
/** Whether the card for the selection is the artifact card: the artifact, or its refusal, changed last. */
|
|
1520
1694
|
showsArtifact() {
|
|
@@ -1591,19 +1765,22 @@ let MosaicaExplorer = (() => {
|
|
|
1591
1765
|
if (!m)
|
|
1592
1766
|
return nothing;
|
|
1593
1767
|
const live = this.liveKey();
|
|
1594
|
-
|
|
1768
|
+
// Pin keeps one name, the card's, and says whether it is on by its pressed state; its hint to a pointer says what a press does.
|
|
1769
|
+
const pinButton = (pressed, name, onPress) => html `<button part="pin" slot="actions" type="button" aria-pressed=${pressed ? 'true' : 'false'} aria-label=${`Pin ${name}`} title=${pressed ? 'Unpin' : 'Keep open'} @click=${onPress}>${icon('pin', 13, 1.5)}</button>`;
|
|
1595
1770
|
const cards = this.pinned
|
|
1596
1771
|
.filter((p) => p.view === view)
|
|
1597
1772
|
.map((p) => {
|
|
1598
|
-
const
|
|
1773
|
+
const name = this.cardName(p.key);
|
|
1774
|
+
const unpin = pinButton(true, name, () => (this.pinned = this.pinned.filter((q) => q.key !== p.key)));
|
|
1599
1775
|
const body = p.kind === 'item'
|
|
1600
1776
|
? this.itemCard(p.item, unpin)
|
|
1601
1777
|
: html `<mosaica-artifact-card exportparts=${FORWARD['artifact-card']} .artifact=${p.artifact}>${unpin}</mosaica-artifact-card>`;
|
|
1602
|
-
return { key: p.key, world: p.world, body, pinned: true, name
|
|
1778
|
+
return { key: p.key, world: p.world, body, pinned: true, name };
|
|
1603
1779
|
});
|
|
1604
1780
|
if (anchor && !(live && this.pinned.some((p) => p.key === live && p.view === view))) {
|
|
1605
|
-
const
|
|
1606
|
-
|
|
1781
|
+
const name = this.cardName(live);
|
|
1782
|
+
const body = live ? this.liveCard(this.showsArtifact(), pinButton(false, name, () => this.togglePin())) : detail;
|
|
1783
|
+
cards.push({ key: 'live', world: anchor, body, pinned: false, name });
|
|
1607
1784
|
}
|
|
1608
1785
|
this.cards = cards.map(({ key, world, pinned }) => ({ key, world, pinned }));
|
|
1609
1786
|
if (cards.length === 0)
|
|
@@ -1618,7 +1795,8 @@ let MosaicaExplorer = (() => {
|
|
|
1618
1795
|
/**
|
|
1619
1796
|
* Place each callout beside its point under the camera as it stands, clear of the cards over the
|
|
1620
1797
|
* map and of the callouts placed before it, and join it to its point. A callout whose point is off
|
|
1621
|
-
* the map is hidden with its leader
|
|
1798
|
+
* the map is hidden with its leader, and gives the focus it holds to the map. Reads no layout: the
|
|
1799
|
+
* sizes are those {@link measure} took.
|
|
1622
1800
|
*/
|
|
1623
1801
|
placeCallouts(firm = this.moving !== null) {
|
|
1624
1802
|
const m = this.map;
|
|
@@ -1641,6 +1819,8 @@ let MosaicaExplorer = (() => {
|
|
|
1641
1819
|
const last = this.placements.get(key);
|
|
1642
1820
|
const kept = last && last.world[0] === card.world[0] && last.world[1] === card.world[1] ? { side: last.side, offset: last.offset, firm } : null;
|
|
1643
1821
|
const at = size.width > 0 && size.height > 0 ? placeCallout(point, box, size, [...this.keepClear, ...placed], undefined, kept, CALLOUT_MIN_HEIGHT) : null;
|
|
1822
|
+
if (at === null && !el.hidden && within(this.renderRoot.activeElement, el))
|
|
1823
|
+
m.focus();
|
|
1644
1824
|
el.hidden = at === null;
|
|
1645
1825
|
leader?.setAttribute('visibility', at === null ? 'hidden' : 'visible');
|
|
1646
1826
|
if (!at)
|
|
@@ -1685,6 +1865,7 @@ let MosaicaExplorer = (() => {
|
|
|
1685
1865
|
});
|
|
1686
1866
|
// A card that grew, as under Show all, keeps its side and grows down where that is clear.
|
|
1687
1867
|
this.placeCallouts();
|
|
1868
|
+
this.coverMap();
|
|
1688
1869
|
}
|
|
1689
1870
|
/** The cards over the map that callouts keep clear of, and the map's tools and the Layers button. */
|
|
1690
1871
|
keptClear() {
|
|
@@ -1708,7 +1889,8 @@ let MosaicaExplorer = (() => {
|
|
|
1708
1889
|
this.measure();
|
|
1709
1890
|
return;
|
|
1710
1891
|
}
|
|
1711
|
-
const
|
|
1892
|
+
const sheet = this.renderRoot.querySelector('[part="sheet"]');
|
|
1893
|
+
const now = new Set([...(m ? [m] : []), ...this.keptClear(), ...callouts, ...(sheet ? [sheet] : [])]);
|
|
1712
1894
|
for (const el of this.observed)
|
|
1713
1895
|
if (!now.has(el))
|
|
1714
1896
|
this.sizes.unobserve(el);
|
|
@@ -1745,24 +1927,9 @@ let MosaicaExplorer = (() => {
|
|
|
1745
1927
|
}
|
|
1746
1928
|
});
|
|
1747
1929
|
}
|
|
1748
|
-
/**
|
|
1749
|
-
* Tab from the map itself goes to the card beside the point, the selection's where it shows, so
|
|
1750
|
-
* the card is the next stop after the map; from there Tab goes on in the page's order.
|
|
1751
|
-
*/
|
|
1752
|
-
onMapKey = (e) => {
|
|
1753
|
-
const from = e.composedPath()[0];
|
|
1754
|
-
if (e.key !== 'Tab' || e.shiftKey || !this.map || (from !== this.map && from !== this.map.focusTarget))
|
|
1755
|
-
return;
|
|
1756
|
-
const shown = Array.from(this.renderRoot.querySelectorAll('[part~="callout"]')).filter((c) => !c.hidden);
|
|
1757
|
-
const card = shown.find((c) => c.dataset.callout === 'live') ?? shown[0];
|
|
1758
|
-
if (!card)
|
|
1759
|
-
return;
|
|
1760
|
-
e.preventDefault();
|
|
1761
|
-
card.focus();
|
|
1762
|
-
};
|
|
1763
1930
|
/** Escape on a card: the card not pinned closes, dropping the selection; focus goes back to the map. */
|
|
1764
1931
|
onCalloutKey = (e) => {
|
|
1765
|
-
if (e.key !== 'Escape')
|
|
1932
|
+
if (e.key !== 'Escape' || e.defaultPrevented)
|
|
1766
1933
|
return;
|
|
1767
1934
|
e.stopPropagation();
|
|
1768
1935
|
const key = e.currentTarget.dataset.callout;
|
|
@@ -1801,11 +1968,15 @@ let MosaicaExplorer = (() => {
|
|
|
1801
1968
|
const change = (patch) => this.changeDisplay(patch);
|
|
1802
1969
|
const number = (e) => Number(e.target.value);
|
|
1803
1970
|
const choices = Object.keys(DENSITY_COLOUR_TITLES);
|
|
1971
|
+
// The arrow keys move the choice and leave the list open; a press chooses and closes it.
|
|
1804
1972
|
const colourList = this.densityColoursOpen
|
|
1805
|
-
? html `<div id="density-colour-list" class="ramp-list" role="radiogroup" aria-labelledby="colours-label">
|
|
1973
|
+
? html `<div id="density-colour-list" class="ramp-list" role="radiogroup" aria-labelledby="colours-label" @focusout=${this.onDensityColoursFocusOut} @keydown=${this.onDensityColoursKey}>
|
|
1806
1974
|
${choices.map((c, i) => html `<button type="button" role="radio" data-colours=${c} aria-checked=${c === colours ? 'true' : 'false'} tabindex=${c === colours ? '0' : '-1'}
|
|
1807
|
-
@click=${() =>
|
|
1808
|
-
|
|
1975
|
+
@click=${() => {
|
|
1976
|
+
change({ densityColours: c });
|
|
1977
|
+
this.closeDensityColours();
|
|
1978
|
+
}}
|
|
1979
|
+
@keydown=${(e) => radioKeys(e, choices.length, i, (j) => change({ densityColours: choices[j] }))}>${tick(c === colours)}<span class="bar" style=${`background:${densityGradient(c, scheme)}`}></span>${DENSITY_COLOUR_TITLES[c]}</button>`)}
|
|
1809
1980
|
</div>`
|
|
1810
1981
|
: nothing;
|
|
1811
1982
|
const densityControls = s.density === 'none'
|
|
@@ -1815,22 +1986,23 @@ let MosaicaExplorer = (() => {
|
|
|
1815
1986
|
${this.densityScaleControl(s.densityScale, change)}
|
|
1816
1987
|
${ramped
|
|
1817
1988
|
? html `<span id="colours-label">Colours</span>
|
|
1818
|
-
<button part="density-colours" class="ramp-choice" type="button" aria-
|
|
1989
|
+
<button part="density-colours" class="ramp-choice" type="button" aria-label=${`Colours: ${DENSITY_COLOUR_TITLES[colours]}`} aria-expanded=${this.densityColoursOpen ? 'true' : 'false'}
|
|
1990
|
+
aria-controls=${this.densityColoursOpen ? 'density-colour-list' : nothing} @focusout=${this.onDensityColoursFocusOut}
|
|
1819
1991
|
@click=${() => (this.densityColoursOpen = !this.densityColoursOpen)}><span class="bar" style=${`background:${densityGradient(colours, scheme)}`}></span>${DENSITY_COLOUR_TITLES[colours]}${icon('chev', 12, 1.4)}</button>
|
|
1820
1992
|
${colourList}`
|
|
1821
1993
|
: nothing}
|
|
1822
1994
|
<label for="density-strength">Strength</label>
|
|
1823
1995
|
<div class="with-readout"><input id="density-strength" part="density-strength" class="slider" type="range" min="0.1" max="1" step="0.05" .value=${String(s.densityStrength)}
|
|
1824
|
-
style=${fill(s.densityStrength, 0.1, 1)} @input=${(e) => change({ densityStrength: number(e) })} /><span class="readout">${Math.round(s.densityStrength * 100)}%</span></div>
|
|
1996
|
+
style=${fill(s.densityStrength, 0.1, 1)} aria-valuetext=${`${Math.round(s.densityStrength * 100)}%`} @input=${(e) => change({ densityStrength: number(e) })} /><span class="readout">${Math.round(s.densityStrength * 100)}%</span></div>
|
|
1825
1997
|
</div>`;
|
|
1826
1998
|
return html `<div part="display" class="display">
|
|
1827
|
-
<section class="sec">
|
|
1999
|
+
<section class="sec" aria-labelledby="points-label">
|
|
1828
2000
|
<div class="sec-head">
|
|
1829
|
-
<
|
|
2001
|
+
<h2 class="hd" id="points-label">Points</h2>
|
|
1830
2002
|
<button part="points-toggle" class="switch small" type="button" role="switch" aria-checked=${s.points ? 'true' : 'false'} aria-label="Show points"
|
|
1831
2003
|
@click=${() => change({ points: !s.points })}><span class="knob"></span></button>
|
|
1832
2004
|
</div>
|
|
1833
|
-
${this.mostPoints(!s.points)}${this.
|
|
2005
|
+
${this.mostPoints(!s.points)}${this.clusterDensity()}
|
|
1834
2006
|
<div class="sliders">
|
|
1835
2007
|
${this.sizeControls(radius, !s.points)}
|
|
1836
2008
|
<label for="point-opacity">Opacity</label>
|
|
@@ -1839,10 +2011,10 @@ let MosaicaExplorer = (() => {
|
|
|
1839
2011
|
</div>
|
|
1840
2012
|
</section>
|
|
1841
2013
|
${this.colourSection()}
|
|
1842
|
-
<section class="sec">
|
|
1843
|
-
<
|
|
2014
|
+
<section class="sec" aria-labelledby="density-label">
|
|
2015
|
+
<h2 class="hd" id="density-label">Density</h2>
|
|
1844
2016
|
<div part="density-mode" class="modes" role="radiogroup" aria-labelledby="density-label">
|
|
1845
|
-
${DENSITY_MODES.map((m, i) => html `<button type="button" role="radio" data-mode=${m.mode} aria-checked=${m.mode === s.density ? 'true' : 'false'} tabindex=${i === modeAt ? '0' : '-1'}
|
|
2017
|
+
${DENSITY_MODES.map((m, i) => html `<button type="button" role="radio" data-mode=${m.mode} aria-checked=${m.mode === s.density ? 'true' : 'false'} tabindex=${i === Math.max(0, modeAt) ? '0' : '-1'}
|
|
1846
2018
|
@click=${() => change({ density: m.mode })}
|
|
1847
2019
|
@keydown=${(e) => radioKeys(e, DENSITY_MODES.length, i, (j) => change({ density: DENSITY_MODES[j].mode }))}>${m.label}</button>`)}
|
|
1848
2020
|
</div>
|
|
@@ -1856,67 +2028,89 @@ let MosaicaExplorer = (() => {
|
|
|
1856
2028
|
*/
|
|
1857
2029
|
mostPoints(disabled) {
|
|
1858
2030
|
const s = this.resolvedStore;
|
|
2031
|
+
const min = Math.max(1, this.budgetMin);
|
|
2032
|
+
const max = Math.max(min * 10, this.budgetMax);
|
|
2033
|
+
const span = Math.log(max / min);
|
|
2034
|
+
const at = (n) => Math.min(1, Math.max(0, Math.log(n / min) / span));
|
|
2035
|
+
const shown = this.pointsDragged ?? s?.budget ?? 250_000;
|
|
2036
|
+
// A tick at each power of ten between the ends, where it leaves room for the last end's label.
|
|
2037
|
+
const ticks = [];
|
|
2038
|
+
for (let p = 10 ** Math.ceil(Math.log10(min) + 1e-9); p < max; p *= 10)
|
|
2039
|
+
if (at(p) > 0.08 && at(p) < 0.85)
|
|
2040
|
+
ticks.push({ at: at(p), text: pointsText(p) });
|
|
1859
2041
|
return this.mostSlider({
|
|
1860
2042
|
id: 'most-points',
|
|
1861
2043
|
label: 'Most points',
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
2044
|
+
at: at(shown),
|
|
2045
|
+
readout: pointsText(shown),
|
|
2046
|
+
valueText: `${pointsText(shown)} points`,
|
|
2047
|
+
ends: [pointsText(min), pointsText(max)],
|
|
2048
|
+
ticks,
|
|
1865
2049
|
disabled: disabled || !s,
|
|
2050
|
+
of: (t) => Math.min(max, Math.max(min, roundPoints(min * Math.exp(t * span)))),
|
|
1866
2051
|
drag: (n) => (this.pointsDragged = n),
|
|
1867
2052
|
set: (n) => this.setBudget(n)
|
|
1868
2053
|
});
|
|
1869
2054
|
}
|
|
1870
2055
|
/**
|
|
1871
|
-
*
|
|
1872
|
-
*
|
|
2056
|
+
* Cluster density, while a `nested` or `dag` layer is drawn, or a levelled layer following the zoom
|
|
2057
|
+
* is drawn or coloured by: a slider over the cluster spacing on a log scale, from
|
|
2058
|
+
* `cluster-spacing-max` at its Fewer end to `cluster-spacing-min` at its More end, with how many
|
|
2059
|
+
* clusters the spacing gives the map at its size. Letting go sets the store's spacing.
|
|
1873
2060
|
*/
|
|
1874
|
-
|
|
2061
|
+
clusterDensity() {
|
|
1875
2062
|
const s = this.resolvedStore;
|
|
1876
2063
|
const meta = s?.get('meta') ?? null;
|
|
1877
2064
|
if (!s || !meta)
|
|
1878
2065
|
return nothing;
|
|
1879
|
-
const
|
|
1880
|
-
|
|
1881
|
-
|
|
2066
|
+
const coloured = clusterLayerOf(s.get('legend').colourBy);
|
|
2067
|
+
const steered = [...s.get('artifacts').layers, ...(coloured === null ? [] : [coloured])].some((name) => {
|
|
2068
|
+
const layer = meta.layers.find((l) => l.name === name);
|
|
2069
|
+
if (!layer)
|
|
2070
|
+
return false;
|
|
2071
|
+
return layer.hierarchy.kind === 'nested' || layer.hierarchy.kind === 'dag' || follows(layer, this.levels.get(name));
|
|
1882
2072
|
});
|
|
1883
|
-
if (!
|
|
2073
|
+
if (!steered)
|
|
1884
2074
|
return nothing;
|
|
2075
|
+
const dense = Math.max(1, this.clusterSpacingMin);
|
|
2076
|
+
const sparse = Math.max(dense * 2, this.clusterSpacingMax);
|
|
2077
|
+
const span = Math.log(sparse / dense);
|
|
2078
|
+
// With no spacing the map shows every cluster, as at the More end.
|
|
2079
|
+
const shown = this.clustersDragged ?? s.clusterSpacing ?? dense;
|
|
2080
|
+
const map = this.renderRoot.querySelector('mosaica-map');
|
|
2081
|
+
const count = map && map.clientWidth > 0 && map.clientHeight > 0 ? clustersInView(shown, map.clientWidth, map.clientHeight) : null;
|
|
2082
|
+
const square = `one per ${Math.round(shown)} px square`;
|
|
1885
2083
|
return this.mostSlider({
|
|
1886
|
-
id: '
|
|
1887
|
-
label: '
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
2084
|
+
id: 'cluster-density',
|
|
2085
|
+
label: 'Cluster density',
|
|
2086
|
+
at: Math.min(1, Math.max(0, Math.log(sparse / shown) / span)),
|
|
2087
|
+
readout: count === null ? `1 per ${Math.round(shown)} px` : pointsText(count),
|
|
2088
|
+
valueText: count === null ? square : `${count} clusters, ${square}`,
|
|
2089
|
+
ends: ['Fewer', 'More'],
|
|
2090
|
+
ticks: [],
|
|
1891
2091
|
disabled: false,
|
|
2092
|
+
of: (t) => Math.min(sparse, Math.max(dense, Math.round(sparse * Math.exp(-t * span)))),
|
|
1892
2093
|
drag: (n) => (this.clustersDragged = n),
|
|
1893
|
-
set: (n) => this.
|
|
2094
|
+
set: (n) => this.setClusterSpacing(n)
|
|
1894
2095
|
});
|
|
1895
2096
|
}
|
|
1896
|
-
/**
|
|
2097
|
+
/**
|
|
2098
|
+
* A slider standing `at` of the way along, with its readout, its ends' labels and its ticks. `of`
|
|
2099
|
+
* gives the value at a share of the way along; dragging passes it to `drag`, and letting go to
|
|
2100
|
+
* `set`.
|
|
2101
|
+
*/
|
|
1897
2102
|
mostSlider(o) {
|
|
1898
|
-
const
|
|
1899
|
-
const max = Math.max(min * 10, o.range[1]);
|
|
1900
|
-
const span = Math.log(max / min);
|
|
1901
|
-
const at = (n) => Math.min(1, Math.max(0, Math.log(n / min) / span));
|
|
1902
|
-
const of = (t) => Math.min(max, Math.max(min, roundPoints(min * Math.exp(t * span))));
|
|
1903
|
-
const t = at(o.shown);
|
|
1904
|
-
// A tick at each power of ten between the ends, where it leaves room for the last end's label.
|
|
1905
|
-
const ticks = [];
|
|
1906
|
-
for (let p = 10 ** Math.ceil(Math.log10(min) + 1e-9); p < max; p *= 10)
|
|
1907
|
-
if (at(p) > 0.08 && at(p) < 0.85)
|
|
1908
|
-
ticks.push(p);
|
|
1909
|
-
const read = (e) => of(Number(e.target.value) / 1000);
|
|
2103
|
+
const read = (e) => o.of(Number(e.target.value) / 1000);
|
|
1910
2104
|
return html `<div class="most">
|
|
1911
|
-
<div class="most-top"><label for=${o.id}>${o.label}</label><span part=${o.id === 'most-points' ? 'most-points-value' : '
|
|
1912
|
-
<input id=${o.id} part=${o.id === 'most-points' ? 'most-points' : '
|
|
1913
|
-
style=${`--fill:${(
|
|
2105
|
+
<div class="most-top"><label for=${o.id}>${o.label}</label><span part=${o.id === 'most-points' ? 'most-points-value' : 'cluster-density-value'} class="most-value">${o.readout}</span></div>
|
|
2106
|
+
<input id=${o.id} part=${o.id === 'most-points' ? 'most-points' : 'cluster-density'} class="slider" type="range" min="0" max="1000" step="1" .value=${String(Math.round(o.at * 1000))} ?disabled=${o.disabled}
|
|
2107
|
+
style=${`--fill:${(o.at * 100).toFixed(1)}%`} aria-valuetext=${o.valueText}
|
|
1914
2108
|
@input=${(e) => o.drag(read(e))}
|
|
1915
2109
|
@change=${(e) => o.set(read(e))} />
|
|
1916
2110
|
<div class="ticks" aria-hidden="true">
|
|
1917
|
-
<span style="left:0">${
|
|
1918
|
-
${ticks.map((p) => html `<span class="mid" style=${`left:${(at
|
|
1919
|
-
<span style="right:0">${
|
|
2111
|
+
<span style="left:0">${o.ends[0]}</span>
|
|
2112
|
+
${o.ticks.map((p) => html `<span class="mid" style=${`left:${(p.at * 100).toFixed(1)}%`}>${p.text}</span>`)}
|
|
2113
|
+
<span style="right:0">${o.ends[1]}</span>
|
|
1920
2114
|
</div>
|
|
1921
2115
|
</div>`;
|
|
1922
2116
|
}
|
|
@@ -1930,15 +2124,15 @@ let MosaicaExplorer = (() => {
|
|
|
1930
2124
|
emit(this, 'mosaica-budgetchange', { budget });
|
|
1931
2125
|
this.requestUpdate();
|
|
1932
2126
|
}
|
|
1933
|
-
/** Set the
|
|
1934
|
-
|
|
2127
|
+
/** Set the store's cluster spacing from Cluster density, and report it. */
|
|
2128
|
+
setClusterSpacing(spacing) {
|
|
1935
2129
|
this.clustersDragged = null;
|
|
1936
2130
|
const s = this.resolvedStore;
|
|
1937
|
-
if (!s ||
|
|
2131
|
+
if (!s || spacing === s.clusterSpacing)
|
|
1938
2132
|
return;
|
|
1939
|
-
s.
|
|
1940
|
-
this.
|
|
1941
|
-
emit(this, 'mosaica-
|
|
2133
|
+
s.setClusterSpacing(spacing);
|
|
2134
|
+
this.clusterSpacing = spacing;
|
|
2135
|
+
emit(this, 'mosaica-clusterspacingchange', { spacing });
|
|
1942
2136
|
}
|
|
1943
2137
|
/**
|
|
1944
2138
|
* The Colour section: Colour by, then the palette for a category, or the ramp, its scale and
|
|
@@ -1956,7 +2150,7 @@ let MosaicaExplorer = (() => {
|
|
|
1956
2150
|
const clusters = colourBy?.startsWith(CLUSTER_PREFIX) ?? false;
|
|
1957
2151
|
const palette = s && clusters ? PALETTES[s.get('artifacts').palette] : CATEGORY_PALETTES[colouring.palette];
|
|
1958
2152
|
const choice = (menu, label, body, stretch = false) => html `<button part=${{ size: 'size-by', colour: 'colour-by', palette: 'palette', ramp: 'ramp' }[menu]} class=${`ramp-choice${stretch ? ' stretch' : ''}`} type="button" aria-haspopup="menu" aria-expanded=${this.menu === menu ? 'true' : 'false'}
|
|
1959
|
-
aria-label=${label} ?disabled=${!meta} @click=${() => (this.menu = this.menu === menu ? null : menu)} @keydown=${this.onMenuButtonKey(menu)}>${body}${icon('chev', 12, 1.4)}</button>`;
|
|
2153
|
+
aria-label=${label} title=${label} ?disabled=${!meta} @click=${() => (this.menu = this.menu === menu ? null : menu)} @keydown=${this.onMenuButtonKey(menu)}>${body}${icon('chev', 12, 1.4)}</button>`;
|
|
1960
2154
|
const byName = current?.title ?? 'None';
|
|
1961
2155
|
const palettes = (column?.category || clusters) && !this.hidePalettes
|
|
1962
2156
|
? html `<span>Palette</span>${choice('palette', `Palette: ${palette.title}`, html `<span class="strip">${palette.colours.slice(0, 10).map((c) => html `<span style=${`background:${hexOf(c)}`}></span>`)}</span><span class="t">${palette.title}</span>`, true)}`
|
|
@@ -1975,8 +2169,8 @@ let MosaicaExplorer = (() => {
|
|
|
1975
2169
|
: nothing;
|
|
1976
2170
|
// A category's values and a layer's clusters have colours of their own to edit.
|
|
1977
2171
|
const editable = Boolean(column?.category) || clusters;
|
|
1978
|
-
return html `<section class="sec">
|
|
1979
|
-
<
|
|
2172
|
+
return html `<section class="sec" aria-labelledby="colour-label">
|
|
2173
|
+
<h2 class="hd" id="colour-label">Colour</h2>
|
|
1980
2174
|
<div class="sliders">
|
|
1981
2175
|
<span>Colour by</span>${choice('colour', `Colour by: ${byName}`, html `<span class="t">${byName}</span>`)}
|
|
1982
2176
|
${palettes}${ramps}
|
|
@@ -1986,7 +2180,8 @@ let MosaicaExplorer = (() => {
|
|
|
1986
2180
|
}
|
|
1987
2181
|
/**
|
|
1988
2182
|
* What Colour by offers: None, every layer that can colour (a levelled layer once per level, by
|
|
1989
|
-
* its levels' titles
|
|
2183
|
+
* its levels' titles, after Follow zoom where no level declares a zoom range), and the rendered
|
|
2184
|
+
* category and number columns.
|
|
1990
2185
|
*/
|
|
1991
2186
|
colourOptions() {
|
|
1992
2187
|
const s = this.resolvedStore;
|
|
@@ -1998,13 +2193,19 @@ let MosaicaExplorer = (() => {
|
|
|
1998
2193
|
const value = `${CLUSTER_PREFIX}${decl.name}`;
|
|
1999
2194
|
if (decl.levels.length <= 1)
|
|
2000
2195
|
return [{ value, title: decl.title || decl.name, kind: 'Clusters', checked: colourBy === value }];
|
|
2001
|
-
const
|
|
2002
|
-
|
|
2196
|
+
const chosen = this.levels.get(decl.name);
|
|
2197
|
+
const following = follows(decl, chosen);
|
|
2198
|
+
const drawn = levelOfLayer(decl, chosen, s?.get('artifacts').followedLevels);
|
|
2199
|
+
const follow = levelFollowsZoom(decl) ? [{ value: `${value}@`, title: `${decl.title || decl.name}, follow zoom`, kind: 'Clusters', checked: colourBy === value && following }] : [];
|
|
2200
|
+
return [...follow, ...decl.levels.map((lv) => ({ value: `${value}@${lv.level}`, title: lv.title || `${decl.title || decl.name}, level ${lv.level}`, kind: 'Clusters', checked: colourBy === value && !following && drawn === lv.level }))];
|
|
2003
2201
|
});
|
|
2004
2202
|
const columns = meta.declaredScalars.filter((c) => c.render).map((c) => ({ value: c.name, title: columnCaption(c.name), kind: c.category ? 'Category' : c.arrowType === 'timestamp_us' ? 'Date' : 'Number', checked: colourBy === c.name }));
|
|
2005
2203
|
return [{ value: '', title: 'None', kind: '', checked: colourBy === null }, ...layers, ...columns];
|
|
2006
2204
|
}
|
|
2007
|
-
/**
|
|
2205
|
+
/**
|
|
2206
|
+
* Colour by `value`: a column, `cluster:<layer>`, `cluster:<layer>@<level>` for one level of a
|
|
2207
|
+
* layer, or `cluster:<layer>@` for the level that follows the zoom.
|
|
2208
|
+
*/
|
|
2008
2209
|
chooseColour(value) {
|
|
2009
2210
|
const s = this.resolvedStore;
|
|
2010
2211
|
if (!s)
|
|
@@ -2013,11 +2214,22 @@ let MosaicaExplorer = (() => {
|
|
|
2013
2214
|
const colourBy = value === '' ? null : at > 0 ? value.slice(0, at) : value;
|
|
2014
2215
|
s.setColourBy(colourBy);
|
|
2015
2216
|
emit(this, 'mosaica-colourchange', { colourBy });
|
|
2016
|
-
if (at > 0) {
|
|
2017
|
-
|
|
2018
|
-
|
|
2217
|
+
if (at > 0 && colourBy !== null) {
|
|
2218
|
+
const layer = colourBy.slice(CLUSTER_PREFIX.length);
|
|
2219
|
+
const level = at === value.length - 1 ? null : Number(value.slice(at + 1));
|
|
2220
|
+
this.chooseLevel(layer, level);
|
|
2221
|
+
emit(this, 'mosaica-levelchange', { layer, level });
|
|
2019
2222
|
}
|
|
2020
2223
|
}
|
|
2224
|
+
/** Count, colour and label `layer` at `level`, or at its default level where `level` is null; other layers keep theirs. */
|
|
2225
|
+
chooseLevel(layer, level) {
|
|
2226
|
+
const levels = new Map(this.levels);
|
|
2227
|
+
if (level === null)
|
|
2228
|
+
levels.delete(layer);
|
|
2229
|
+
else
|
|
2230
|
+
levels.set(layer, level);
|
|
2231
|
+
this.levels = levels;
|
|
2232
|
+
}
|
|
2021
2233
|
/** Colour a layer's clusters from `palette`, and report it. */
|
|
2022
2234
|
chooseClusterPalette(palette) {
|
|
2023
2235
|
const s = this.resolvedStore;
|
|
@@ -2084,7 +2296,7 @@ let MosaicaExplorer = (() => {
|
|
|
2084
2296
|
const at = DENSITY_SCALES.findIndex((x) => x.scale === scale);
|
|
2085
2297
|
return html `<span id="density-scale-label">Scale</span>
|
|
2086
2298
|
<div part="density-scale" class="seg" role="radiogroup" aria-labelledby="density-scale-label">
|
|
2087
|
-
${DENSITY_SCALES.map((x, i) => html `<button type="button" role="radio" data-scale=${x.scale} aria-checked=${x.scale === scale ? 'true' : 'false'} tabindex=${i === at ? '0' : '-1'}
|
|
2299
|
+
${DENSITY_SCALES.map((x, i) => html `<button type="button" role="radio" data-scale=${x.scale} aria-checked=${x.scale === scale ? 'true' : 'false'} tabindex=${i === Math.max(0, at) ? '0' : '-1'}
|
|
2088
2300
|
@click=${() => change({ densityScale: x.scale })}
|
|
2089
2301
|
@keydown=${(e) => radioKeys(e, DENSITY_SCALES.length, i, (j) => change({ densityScale: DENSITY_SCALES[j].scale }))}>${x.label}</button>`)}
|
|
2090
2302
|
</div>`;
|
|
@@ -2113,10 +2325,15 @@ let MosaicaExplorer = (() => {
|
|
|
2113
2325
|
const sizing = this.sizing;
|
|
2114
2326
|
const number = (e) => Number(e.target.value);
|
|
2115
2327
|
const { min, max, step } = SIZE_RANGE;
|
|
2328
|
+
// With no column to offer, the button says why: to a pointer in its `title`, and in the text that describes it.
|
|
2329
|
+
const none = this.resolvedStore?.get('meta') && this.sizeColumns.length === 0;
|
|
2330
|
+
const why = 'No number column to size by';
|
|
2331
|
+
const shown = sizeBy === null ? 'None' : columnCaption(sizeBy);
|
|
2116
2332
|
const choice = html `<span id="size-by-label">Size by</span>
|
|
2117
|
-
<button part="size-by" class="ramp-choice" type="button" aria-haspopup="menu" aria-expanded=${this.menu === 'size' ? 'true' : 'false'} aria-label=${`Size by: ${
|
|
2118
|
-
?disabled=${disabled || (sizeBy === null && this.sizeColumns.length === 0)} title=${
|
|
2119
|
-
@click=${() => (this.menu = this.menu === 'size' ? null : 'size')} @keydown=${this.onMenuButtonKey('size')}><span class="t">${
|
|
2333
|
+
<button part="size-by" class="ramp-choice" type="button" aria-haspopup="menu" aria-expanded=${this.menu === 'size' ? 'true' : 'false'} aria-label=${`Size by: ${shown}`}
|
|
2334
|
+
?disabled=${disabled || (sizeBy === null && this.sizeColumns.length === 0)} title=${none ? why : shown} aria-describedby=${none ? 'size-by-note' : nothing}
|
|
2335
|
+
@click=${() => (this.menu = this.menu === 'size' ? null : 'size')} @keydown=${this.onMenuButtonKey('size')}><span class="t">${shown}</span>${icon('chev', 12, 1.4)}</button>
|
|
2336
|
+
${none ? html `<span id="size-by-note" class="sr-only">${why}</span>` : nothing}`;
|
|
2120
2337
|
if (sizeBy === null) {
|
|
2121
2338
|
if (this.hideSize)
|
|
2122
2339
|
return choice;
|
|
@@ -2129,14 +2346,14 @@ let MosaicaExplorer = (() => {
|
|
|
2129
2346
|
return html `${choice}<span id="size-range-label">Range</span>
|
|
2130
2347
|
<div class="range" role="group" aria-labelledby="size-range-label">
|
|
2131
2348
|
<input part="size-min" class="slider" type="range" min=${min} max=${max} step=${step} aria-label="Smallest size" .value=${String(sizing.min)} ?disabled=${disabled}
|
|
2132
|
-
style=${fill(sizing.min, min, max)} @input=${(e) => this.changeSize({ min: number(e), max: Math.max(number(e), sizing.max) })} />
|
|
2349
|
+
style=${fill(sizing.min, min, max)} aria-valuetext=${`${px(sizing.min)} px`} @input=${(e) => this.changeSize({ min: number(e), max: Math.max(number(e), sizing.max) })} />
|
|
2133
2350
|
<span part="size-range" class="readout">${px(sizing.min)} – ${px(sizing.max)} px</span>
|
|
2134
2351
|
<input part="size-max" class="slider" type="range" min=${min} max=${max} step=${step} aria-label="Largest size" .value=${String(sizing.max)} ?disabled=${disabled}
|
|
2135
|
-
style=${fill(sizing.max, min, max)} @input=${(e) => this.changeSize({ max: number(e), min: Math.min(number(e), sizing.min) })} />
|
|
2352
|
+
style=${fill(sizing.max, min, max)} aria-valuetext=${`${px(sizing.max)} px`} @input=${(e) => this.changeSize({ max: number(e), min: Math.min(number(e), sizing.min) })} />
|
|
2136
2353
|
</div>
|
|
2137
2354
|
<span id="size-scale-label">Scale</span>
|
|
2138
2355
|
<div part="size-scale" class="seg" role="radiogroup" aria-labelledby="size-scale-label">
|
|
2139
|
-
${SIZE_SCALES.map((x, i) => html `<button type="button" role="radio" data-scale=${x.scale} aria-checked=${x.scale === sizing.scale ? 'true' : 'false'} tabindex=${i === at ? '0' : '-1'} ?disabled=${disabled}
|
|
2356
|
+
${SIZE_SCALES.map((x, i) => html `<button type="button" role="radio" data-scale=${x.scale} aria-checked=${x.scale === sizing.scale ? 'true' : 'false'} tabindex=${i === Math.max(0, at) ? '0' : '-1'} ?disabled=${disabled}
|
|
2140
2357
|
@click=${() => this.changeSize({ scale: x.scale })}
|
|
2141
2358
|
@keydown=${(e) => radioKeys(e, SIZE_SCALES.length, i, (j) => this.changeSize({ scale: SIZE_SCALES[j].scale }))}>${x.label}</button>`)}
|
|
2142
2359
|
</div>`;
|
|
@@ -2209,10 +2426,19 @@ let MosaicaExplorer = (() => {
|
|
|
2209
2426
|
const items = Array.from(this.renderRoot.querySelectorAll(`[part~="${menu}-option"]`));
|
|
2210
2427
|
const next = { ArrowDown: (i + 1) % items.length, ArrowUp: (i - 1 + items.length) % items.length, Home: 0, End: items.length - 1 }[e.key];
|
|
2211
2428
|
if (e.key === 'Escape') {
|
|
2429
|
+
// An Escape that has dismissed the map's tooltip does no more.
|
|
2430
|
+
if (e.defaultPrevented)
|
|
2431
|
+
return;
|
|
2212
2432
|
e.stopPropagation();
|
|
2213
2433
|
this.closeMenu(true);
|
|
2214
2434
|
return;
|
|
2215
2435
|
}
|
|
2436
|
+
// Tab closes the menu and goes back to its button, in the popover, and on from there.
|
|
2437
|
+
if (e.key === 'Tab') {
|
|
2438
|
+
e.preventDefault();
|
|
2439
|
+
this.closeMenu(true);
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2216
2442
|
if (next === undefined)
|
|
2217
2443
|
return;
|
|
2218
2444
|
e.preventDefault();
|
|
@@ -2221,8 +2447,8 @@ let MosaicaExplorer = (() => {
|
|
|
2221
2447
|
};
|
|
2222
2448
|
const at = Math.max(0, entries.findIndex((o) => o.value === checked));
|
|
2223
2449
|
const row = (o) => o.line === undefined
|
|
2224
|
-
? html `<span class="lead">${o.swatch ?? nothing}<span>${o.title}</span></span><span class="kind">${o.kind}</span>`
|
|
2225
|
-
: html `${o.swatch ?? nothing}<span class="named"><span class="t">${o.title}</span><span class="line">${o.line}</span></span>`;
|
|
2450
|
+
? html `<span class="lead">${tick(o.value === checked)}${o.swatch ?? nothing}<span>${o.title}</span></span><span class="kind">${o.kind}</span>`
|
|
2451
|
+
: html `${tick(o.value === checked)}${o.swatch ?? nothing}<span class="named"><span class="t">${o.title}</span><span class="line">${o.line}</span></span>`;
|
|
2226
2452
|
return html `<div part=${{ size: 'size-menu', colour: 'colour-menu', palette: 'palette-menu', ramp: 'ramp-menu' }[menu]} class=${`size-menu${menu === 'ramp' ? ' wide' : menu === 'palette' ? ' palettes' : ''}`} popover="manual" role="menu" aria-labelledby="menu-label" @focusout=${this.onMenuFocusOut}>
|
|
2227
2453
|
<div class="hd" id="menu-label">${title}</div>
|
|
2228
2454
|
${entries.map((o, i) => html `<button part=${{ size: 'size-option', colour: 'colour-option', palette: 'palette-option', ramp: 'ramp-option' }[menu]} type="button" role="menuitemradio" data-value=${o.value} aria-checked=${o.value === checked ? 'true' : 'false'}
|
|
@@ -2245,7 +2471,7 @@ let MosaicaExplorer = (() => {
|
|
|
2245
2471
|
this.menu = menu;
|
|
2246
2472
|
};
|
|
2247
2473
|
}
|
|
2248
|
-
/** Focus leaving the open menu
|
|
2474
|
+
/** Focus leaving the open menu closes it. */
|
|
2249
2475
|
onMenuFocusOut = (e) => {
|
|
2250
2476
|
const to = e.relatedTarget;
|
|
2251
2477
|
if (to && e.currentTarget.contains(to))
|
|
@@ -2291,10 +2517,8 @@ let MosaicaExplorer = (() => {
|
|
|
2291
2517
|
this.pointOpacity = patch.pointOpacity;
|
|
2292
2518
|
if (patch.density !== undefined)
|
|
2293
2519
|
this.density = patch.density;
|
|
2294
|
-
if (patch.densityColours !== undefined)
|
|
2520
|
+
if (patch.densityColours !== undefined)
|
|
2295
2521
|
this.densityColours = patch.densityColours ?? '';
|
|
2296
|
-
this.densityColoursOpen = false;
|
|
2297
|
-
}
|
|
2298
2522
|
if (patch.densityStrength !== undefined)
|
|
2299
2523
|
this.densityStrength = patch.densityStrength;
|
|
2300
2524
|
if (patch.densityResolution !== undefined)
|
|
@@ -2303,17 +2527,36 @@ let MosaicaExplorer = (() => {
|
|
|
2303
2527
|
this.densityScale = patch.densityScale;
|
|
2304
2528
|
emit(this, 'mosaica-displaychange', this.display);
|
|
2305
2529
|
}
|
|
2530
|
+
/** Close the list of density colours, with focus on its button. */
|
|
2531
|
+
closeDensityColours() {
|
|
2532
|
+
this.renderRoot.querySelector('[part="density-colours"]')?.focus();
|
|
2533
|
+
this.densityColoursOpen = false;
|
|
2534
|
+
}
|
|
2535
|
+
/** Escape in the list of density colours closes the list alone. */
|
|
2536
|
+
onDensityColoursKey = (e) => {
|
|
2537
|
+
if (e.key !== 'Escape' || e.defaultPrevented)
|
|
2538
|
+
return;
|
|
2539
|
+
e.stopPropagation();
|
|
2540
|
+
this.closeDensityColours();
|
|
2541
|
+
};
|
|
2542
|
+
/** Focus leaving the list of density colours and its button closes the list. */
|
|
2543
|
+
onDensityColoursFocusOut = (e) => {
|
|
2544
|
+
const to = e.relatedTarget;
|
|
2545
|
+
const held = [this.renderRoot.querySelector('#density-colour-list'), this.renderRoot.querySelector('[part="density-colours"]')];
|
|
2546
|
+
if (!held.some((el) => el && to && el.contains(to)))
|
|
2547
|
+
this.densityColoursOpen = false;
|
|
2548
|
+
};
|
|
2306
2549
|
/** Escape closes the Layers popover and returns focus to its button. */
|
|
2307
2550
|
onLayersKey = (e) => {
|
|
2308
|
-
if (e.key !== 'Escape' || !this.layersOpen)
|
|
2551
|
+
if (e.key !== 'Escape' || e.defaultPrevented || !this.layersOpen)
|
|
2309
2552
|
return;
|
|
2310
2553
|
e.stopPropagation();
|
|
2311
2554
|
this.closeLayers();
|
|
2312
2555
|
this.renderRoot.querySelector('[part="layers-toggle"]')?.focus();
|
|
2313
2556
|
};
|
|
2314
|
-
/** The
|
|
2557
|
+
/** The bar's keys: the arrows move between its buttons, wrapping, and Home and End go to the ends. */
|
|
2315
2558
|
onTabKey = (e) => {
|
|
2316
|
-
const buttons = Array.from(this.renderRoot.querySelectorAll('[part="tabs"]
|
|
2559
|
+
const buttons = Array.from(this.renderRoot.querySelectorAll('[part="tabs"] button'));
|
|
2317
2560
|
const at = buttons.indexOf(e.target);
|
|
2318
2561
|
if (at < 0)
|
|
2319
2562
|
return;
|
|
@@ -2328,24 +2571,56 @@ let MosaicaExplorer = (() => {
|
|
|
2328
2571
|
};
|
|
2329
2572
|
/** Escape closes the sheet. */
|
|
2330
2573
|
onSheetKey = (e) => {
|
|
2331
|
-
if (e.key !== 'Escape')
|
|
2574
|
+
if (e.key !== 'Escape' || e.defaultPrevented)
|
|
2332
2575
|
return;
|
|
2333
2576
|
e.stopPropagation();
|
|
2334
2577
|
this.sheet = null;
|
|
2335
2578
|
};
|
|
2336
|
-
/**
|
|
2579
|
+
/**
|
|
2580
|
+
* Focus goes to the first control of the Layers popover as it opens. A sheet that opens takes
|
|
2581
|
+
* the focus the explorer held, and none from the page around it. A sheet that closes gives the
|
|
2582
|
+
* focus it held to its button, or to the map where the layout has grown wide and has no button.
|
|
2583
|
+
*/
|
|
2337
2584
|
updated(changed) {
|
|
2338
2585
|
this.toggleAttribute('data-compact', this.compact);
|
|
2339
2586
|
this.fitLayers();
|
|
2340
2587
|
this.placeMenu(changed.has('menu') && this.menu !== null);
|
|
2341
2588
|
this.followLayout();
|
|
2589
|
+
this.coverMap();
|
|
2590
|
+
if (changed.has('layersOpen') && this.layersOpen)
|
|
2591
|
+
this.renderRoot.querySelector('[part="layers-popover"]')?.querySelector(FOCUSABLE)?.focus();
|
|
2342
2592
|
if (!changed.has('sheet'))
|
|
2343
2593
|
return;
|
|
2344
2594
|
const before = changed.get('sheet');
|
|
2345
|
-
if (this.sheet)
|
|
2346
|
-
this.
|
|
2347
|
-
|
|
2348
|
-
|
|
2595
|
+
if (this.sheet) {
|
|
2596
|
+
if (this.focusBefore)
|
|
2597
|
+
this.renderRoot.querySelector('[part="sheet"]')?.focus();
|
|
2598
|
+
}
|
|
2599
|
+
else if (before && this.focusBefore === 'sheet') {
|
|
2600
|
+
if (this.narrow)
|
|
2601
|
+
this.renderRoot.querySelector(`[part="tabs"] [data-sheet="${before}"]`)?.focus();
|
|
2602
|
+
else
|
|
2603
|
+
this.map?.focus();
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
/**
|
|
2607
|
+
* While an open sheet reaches up over the map's tools, the map behind it is inert: nothing under
|
|
2608
|
+
* the sheet takes focus or a press. A shorter sheet leaves the map above it in use.
|
|
2609
|
+
*/
|
|
2610
|
+
coverMap() {
|
|
2611
|
+
const m = this.map;
|
|
2612
|
+
if (!m)
|
|
2613
|
+
return;
|
|
2614
|
+
const sheet = this.renderRoot.querySelector('[part="sheet"]');
|
|
2615
|
+
// Nothing is measured where no sheet is open, which is every draw of the wide layouts.
|
|
2616
|
+
if (!sheet) {
|
|
2617
|
+
if (m.inert)
|
|
2618
|
+
m.inert = false;
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
const tools = m.shadowRoot?.querySelector('[part="controls"]');
|
|
2622
|
+
const clear = tools ? tools.getBoundingClientRect().bottom : m.getBoundingClientRect().top;
|
|
2623
|
+
m.inert = sheet.getBoundingClientRect().top < clear;
|
|
2349
2624
|
}
|
|
2350
2625
|
/**
|
|
2351
2626
|
* Hold the open Layers popover within the explorer's visible box: the part of it inside the
|