@mosaicajs/components 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/aggregate.d.ts +4 -10
- package/dist/aggregate.js +7 -13
- package/dist/artifact-card.d.ts +21 -7
- package/dist/artifact-card.js +136 -79
- package/dist/base.d.ts +44 -5
- package/dist/base.js +54 -5
- package/dist/cluster-filter.d.ts +29 -25
- package/dist/cluster-filter.js +123 -106
- package/dist/colour-editor.d.ts +15 -4
- package/dist/colour-editor.js +107 -34
- package/dist/colour-picker.d.ts +12 -5
- package/dist/colour-picker.js +119 -32
- package/dist/display.js +77 -9
- package/dist/events.d.ts +21 -7
- package/dist/explorer.d.ts +116 -37
- package/dist/explorer.js +476 -210
- package/dist/field-card.d.ts +304 -48
- package/dist/field-card.js +1364 -244
- package/dist/filter-panel.d.ts +54 -10
- package/dist/filter-panel.js +242 -42
- package/dist/filter.d.ts +22 -4
- package/dist/filter.js +113 -34
- package/dist/hierarchy.d.ts +68 -45
- package/dist/hierarchy.js +320 -243
- package/dist/item-card.d.ts +47 -16
- package/dist/item-card.js +241 -108
- package/dist/key-picker.d.ts +2 -1
- package/dist/key-picker.js +4 -3
- package/dist/layer-picker.d.ts +4 -2
- package/dist/layer-picker.js +16 -7
- package/dist/lineage.d.ts +132 -0
- package/dist/lineage.js +307 -0
- package/dist/map.d.ts +135 -24
- package/dist/map.js +387 -117
- package/dist/mosaica-components.js +1837 -833
- package/dist/mosaica-components.js.sri +1 -1
- package/dist/mosaica-components.json +2 -2
- package/dist/parts.d.ts +4 -4
- package/dist/parts.js +18 -4
- package/dist/paths.d.ts +34 -12
- package/dist/paths.js +84 -46
- package/dist/selection.d.ts +22 -3
- package/dist/selection.js +125 -59
- package/dist/states.d.ts +72 -0
- package/dist/states.js +135 -26
- package/dist/status.d.ts +63 -5
- package/dist/status.js +215 -36
- package/dist/tokens.d.ts +8 -4
- package/dist/tokens.js +144 -14
- package/dist/view-picker.js +5 -0
- package/package.json +4 -3
package/dist/filter-panel.js
CHANGED
|
@@ -38,10 +38,11 @@ import { repeat } from 'lit/directives/repeat.js';
|
|
|
38
38
|
import { CLUSTER_PREFIX, activeCount, artifactName, emptyDraft, isPopulated, withoutClause, withoutMember } from '@mosaicajs/client';
|
|
39
39
|
import { OPERATOR_WORDS } from './filter.js';
|
|
40
40
|
import { HeldAggregate } from './aggregate.js';
|
|
41
|
-
import { MosaicaElement,
|
|
41
|
+
import { BOOL_WORDS, MosaicaElement, boolKeys, called, columnCaption, dateRangeText, countText, emit, focusLost, keyTitle, shortCount } from './base.js';
|
|
42
42
|
import { attachContextRoot, defineOnce } from './define.js';
|
|
43
43
|
import { icon } from './icons.js';
|
|
44
44
|
import { exportparts, forwarded } from './parts.js';
|
|
45
|
+
import { switchView } from './view-switch.js';
|
|
45
46
|
import { FloatingList } from './float.js';
|
|
46
47
|
import { renderState, stateOf } from './states.js';
|
|
47
48
|
import { chrome, tokens } from './tokens.js';
|
|
@@ -55,12 +56,27 @@ export function clusterFieldLayers(meta, view) {
|
|
|
55
56
|
/** A layer's field as the panel keys it, apart from any column's. */
|
|
56
57
|
const layerField = (layer) => `${CLUSTER_PREFIX}${layer}`;
|
|
57
58
|
const VERBS = ['filter', 'highlight'];
|
|
59
|
+
/** A view's name as the view picker shows it, else its id. */
|
|
60
|
+
const viewTitle = (meta, id) => meta.views.find((v) => v.id === id)?.displayName ?? id;
|
|
61
|
+
/** A held region's chip text: its shape and the view it was drawn in. */
|
|
62
|
+
function regionText(shape, view) {
|
|
63
|
+
const kind = shape.kind === 'box' ? 'Box' : shape.kind === 'lasso' ? 'Lasso' : 'Shape';
|
|
64
|
+
return `${shape.outside ? `Outside ${kind.toLowerCase()}` : kind} in ${view}`;
|
|
65
|
+
}
|
|
58
66
|
/** A figure in the subject row: whole, or shortened in the compact layout. */
|
|
59
67
|
const figure = (n, compact) => (compact ? shortCount(n) : countText(n));
|
|
60
68
|
/**
|
|
61
69
|
* Use the filter panel as the place where viewers narrow the map down, as `<mosaica-explorer>` does
|
|
62
|
-
* in its sidebar. Each filter or highlight in force shows as a chip above the cards,
|
|
63
|
-
* chip opens its field's card.
|
|
70
|
+
* in its sidebar. Each filter or highlight in force shows as a chip above the cards, a highlight's
|
|
71
|
+
* with a pen on it, and pressing a chip opens its field's card. When a chip is removed, focus
|
|
72
|
+
* moves to the × of the chip after it, else of the one before, else to Add field. Under
|
|
73
|
+
* `chips-only` there is no Add field, so when the last chip is removed the panel shows the words
|
|
74
|
+
* No filters, which take focus and go as focus leaves them; a chip that arrives while they hold
|
|
75
|
+
* focus takes it on its ×.
|
|
76
|
+
*
|
|
77
|
+
* A region selected in another view keeps filtering after the viewer switches away from it, and
|
|
78
|
+
* shows as a chip naming its view, such as "Lasso in Topics". Pressing the chip switches back to
|
|
79
|
+
* that view, where the region is selected again, and its × removes it.
|
|
64
80
|
*
|
|
65
81
|
* The row at the top says what the cards' solid bars count. That is the items in view that the
|
|
66
82
|
* filters match, or the highlighted items while there is a highlight. The pale bars count
|
|
@@ -70,6 +86,10 @@ const figure = (n, compact) => (compact ? shortCount(n) : countText(n));
|
|
|
70
86
|
* highlight, and for the field the map is coloured by. Viewers add other fields with Add field.
|
|
71
87
|
* Taking a card off clears that field's filters and highlights.
|
|
72
88
|
*
|
|
89
|
+
* The panel sets `hide-chips` on the cards it shows, so that a layer's card does not repeat the
|
|
90
|
+
* chips the panel shows for its clauses. Under `controls-only` the panel shows no chips, and the
|
|
91
|
+
* cards keep theirs.
|
|
92
|
+
*
|
|
73
93
|
* Set `chips-only` on one panel and `controls-only` on another when you want the chips apart from
|
|
74
94
|
* the cards.
|
|
75
95
|
*
|
|
@@ -80,6 +100,10 @@ const figure = (n, compact) => (compact ? shortCount(n) : countText(n));
|
|
|
80
100
|
* column's chip was removed, Clear all was pressed, the highlight was cleared from the subject
|
|
81
101
|
* row, or a card was taken off in Add field. Removing a `member_of` chip fires nothing. Each card
|
|
82
102
|
* fires its own as well.
|
|
103
|
+
* @fires {CustomEvent<MosaicaEventDetails['mosaica-selectchange']>} mosaica-selectchange - The chip
|
|
104
|
+
* of a region held in another view was removed, or Clear all removed one, with that `view`.
|
|
105
|
+
* @fires {CustomEvent<MosaicaEventDetails['mosaica-viewswitch']>} mosaica-viewswitch - The chip of
|
|
106
|
+
* a region held in another view was pressed, and the panel switched to that view.
|
|
83
107
|
* @fires {CustomEvent<MosaicaEventDetails['mosaica-chipopen']>} mosaica-chipopen - A column's chip
|
|
84
108
|
* was pressed while `chips-only` is set, so you can show its card.
|
|
85
109
|
* @csspart state - The state line, with `data-state`.
|
|
@@ -92,15 +116,18 @@ const figure = (n, compact) => (compact ? shortCount(n) : countText(n));
|
|
|
92
116
|
* @csspart clear-highlight - The × beside Highlighted, which clears the highlight.
|
|
93
117
|
* @csspart all - All matching and its number.
|
|
94
118
|
* @csspart all-count - The number matching.
|
|
95
|
-
* @csspart chips - The line of chips, while any filter or highlight is in force
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
119
|
+
* @csspart chips - The line of chips, while any filter or highlight is in force, and under
|
|
120
|
+
* `chips-only` the words that stand in it after the last chip is removed.
|
|
121
|
+
* @csspart chip - One chip, with `data-verb` (`filter` or `highlight`) and `data-column`,
|
|
122
|
+
* `data-artifact`, or `data-view` for a region held in another view.
|
|
123
|
+
* @csspart edit - A column chip's text, a button that opens its card; on a held region's chip, a
|
|
124
|
+
* button that switches to its view.
|
|
99
125
|
* @csspart clear - The Clear all button.
|
|
100
126
|
* @csspart card - One `<mosaica-field-card>`, with `data-field`.
|
|
101
127
|
* @csspart add - The Add field button, with `aria-expanded`.
|
|
102
128
|
* @csspart add-note - How many fields are not listed.
|
|
103
|
-
* @csspart add-list - The list of fields to add, while it is open.
|
|
129
|
+
* @csspart add-list - The list of fields to add, while it is open. A press outside it, or focus
|
|
130
|
+
* leaving it, closes it.
|
|
104
131
|
* @csspart add-search - The search box over that list.
|
|
105
132
|
* @csspart add-option - One field in that list, with `data-column` or `data-layer`, `aria-checked`
|
|
106
133
|
* while its card is shown, and `aria-disabled` if it is pinned and cannot be taken off.
|
|
@@ -122,9 +149,9 @@ let MosaicaFilterPanel = (() => {
|
|
|
122
149
|
let _compact_decorators;
|
|
123
150
|
let _compact_initializers = [];
|
|
124
151
|
let _compact_extraInitializers = [];
|
|
125
|
-
let
|
|
126
|
-
let
|
|
127
|
-
let
|
|
152
|
+
let _clusterLevels_decorators;
|
|
153
|
+
let _clusterLevels_initializers = [];
|
|
154
|
+
let _clusterLevels_extraInitializers = [];
|
|
128
155
|
let _opened_decorators;
|
|
129
156
|
let _opened_initializers = [];
|
|
130
157
|
let _opened_extraInitializers = [];
|
|
@@ -143,6 +170,9 @@ let MosaicaFilterPanel = (() => {
|
|
|
143
170
|
let _addActive_decorators;
|
|
144
171
|
let _addActive_initializers = [];
|
|
145
172
|
let _addActive_extraInitializers = [];
|
|
173
|
+
let _emptied_decorators;
|
|
174
|
+
let _emptied_initializers = [];
|
|
175
|
+
let _emptied_extraInitializers = [];
|
|
146
176
|
return class MosaicaFilterPanel extends _classSuper {
|
|
147
177
|
static {
|
|
148
178
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
@@ -150,24 +180,26 @@ let MosaicaFilterPanel = (() => {
|
|
|
150
180
|
_controlsOnly_decorators = [property({ type: Boolean, attribute: 'controls-only' })];
|
|
151
181
|
_pinned_decorators = [property()];
|
|
152
182
|
_compact_decorators = [property({ type: Boolean, reflect: true })];
|
|
153
|
-
|
|
183
|
+
_clusterLevels_decorators = [property({ attribute: false })];
|
|
154
184
|
_opened_decorators = [state()];
|
|
155
185
|
_folded_decorators = [state()];
|
|
156
186
|
_openCard_decorators = [state()];
|
|
157
187
|
_adding_decorators = [state()];
|
|
158
188
|
_addSearch_decorators = [state()];
|
|
159
189
|
_addActive_decorators = [state()];
|
|
190
|
+
_emptied_decorators = [state()];
|
|
160
191
|
__esDecorate(this, null, _chipsOnly_decorators, { kind: "accessor", name: "chipsOnly", static: false, private: false, access: { has: obj => "chipsOnly" in obj, get: obj => obj.chipsOnly, set: (obj, value) => { obj.chipsOnly = value; } }, metadata: _metadata }, _chipsOnly_initializers, _chipsOnly_extraInitializers);
|
|
161
192
|
__esDecorate(this, null, _controlsOnly_decorators, { kind: "accessor", name: "controlsOnly", static: false, private: false, access: { has: obj => "controlsOnly" in obj, get: obj => obj.controlsOnly, set: (obj, value) => { obj.controlsOnly = value; } }, metadata: _metadata }, _controlsOnly_initializers, _controlsOnly_extraInitializers);
|
|
162
193
|
__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);
|
|
163
194
|
__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);
|
|
164
|
-
__esDecorate(this, null,
|
|
195
|
+
__esDecorate(this, null, _clusterLevels_decorators, { kind: "accessor", name: "clusterLevels", static: false, private: false, access: { has: obj => "clusterLevels" in obj, get: obj => obj.clusterLevels, set: (obj, value) => { obj.clusterLevels = value; } }, metadata: _metadata }, _clusterLevels_initializers, _clusterLevels_extraInitializers);
|
|
165
196
|
__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);
|
|
166
197
|
__esDecorate(this, null, _folded_decorators, { kind: "accessor", name: "folded", static: false, private: false, access: { has: obj => "folded" in obj, get: obj => obj.folded, set: (obj, value) => { obj.folded = value; } }, metadata: _metadata }, _folded_initializers, _folded_extraInitializers);
|
|
167
198
|
__esDecorate(this, null, _openCard_decorators, { kind: "accessor", name: "openCard", static: false, private: false, access: { has: obj => "openCard" in obj, get: obj => obj.openCard, set: (obj, value) => { obj.openCard = value; } }, metadata: _metadata }, _openCard_initializers, _openCard_extraInitializers);
|
|
168
199
|
__esDecorate(this, null, _adding_decorators, { kind: "accessor", name: "adding", static: false, private: false, access: { has: obj => "adding" in obj, get: obj => obj.adding, set: (obj, value) => { obj.adding = value; } }, metadata: _metadata }, _adding_initializers, _adding_extraInitializers);
|
|
169
200
|
__esDecorate(this, null, _addSearch_decorators, { kind: "accessor", name: "addSearch", static: false, private: false, access: { has: obj => "addSearch" in obj, get: obj => obj.addSearch, set: (obj, value) => { obj.addSearch = value; } }, metadata: _metadata }, _addSearch_initializers, _addSearch_extraInitializers);
|
|
170
201
|
__esDecorate(this, null, _addActive_decorators, { kind: "accessor", name: "addActive", static: false, private: false, access: { has: obj => "addActive" in obj, get: obj => obj.addActive, set: (obj, value) => { obj.addActive = value; } }, metadata: _metadata }, _addActive_initializers, _addActive_extraInitializers);
|
|
202
|
+
__esDecorate(this, null, _emptied_decorators, { kind: "accessor", name: "emptied", static: false, private: false, access: { has: obj => "emptied" in obj, get: obj => obj.emptied, set: (obj, value) => { obj.emptied = value; } }, metadata: _metadata }, _emptied_initializers, _emptied_extraInitializers);
|
|
171
203
|
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
172
204
|
}
|
|
173
205
|
#chipsOnly_accessor_storage = __runInitializers(this, _chipsOnly_initializers, false);
|
|
@@ -190,11 +222,15 @@ let MosaicaFilterPanel = (() => {
|
|
|
190
222
|
/** Keeps one card open at a time, and shortens the subject row's figures. */
|
|
191
223
|
get compact() { return this.#compact_accessor_storage; }
|
|
192
224
|
set compact(value) { this.#compact_accessor_storage = value; }
|
|
193
|
-
#
|
|
194
|
-
/**
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
225
|
+
#clusterLevels_accessor_storage = (__runInitializers(this, _compact_extraInitializers), __runInitializers(this, _clusterLevels_initializers, new Map()));
|
|
226
|
+
/**
|
|
227
|
+
* The level each card for a `stacked` or `tiered` layer counts at, keyed by layer name. A layer
|
|
228
|
+
* with no entry counts at the level the map draws it at, which follows the zoom where its levels
|
|
229
|
+
* declare no zoom range. Set it as a property; it has no attribute.
|
|
230
|
+
*/
|
|
231
|
+
get clusterLevels() { return this.#clusterLevels_accessor_storage; }
|
|
232
|
+
set clusterLevels(value) { this.#clusterLevels_accessor_storage = value; }
|
|
233
|
+
#opened_accessor_storage = (__runInitializers(this, _clusterLevels_extraInitializers), __runInitializers(this, _opened_initializers, new Set()));
|
|
198
234
|
/** The fields the user added, which stay listed. @internal */
|
|
199
235
|
get opened() { return this.#opened_accessor_storage; }
|
|
200
236
|
set opened(value) { this.#opened_accessor_storage = value; }
|
|
@@ -257,6 +293,7 @@ let MosaicaFilterPanel = (() => {
|
|
|
257
293
|
height: 6px;
|
|
258
294
|
border-radius: 1px;
|
|
259
295
|
background: var(--_mosaica-bar);
|
|
296
|
+
forced-color-adjust: none;
|
|
260
297
|
}
|
|
261
298
|
.swatch.lit {
|
|
262
299
|
background: var(--_mosaica-bar-highlight);
|
|
@@ -276,6 +313,7 @@ let MosaicaFilterPanel = (() => {
|
|
|
276
313
|
color: var(--_mosaica-ink);
|
|
277
314
|
}
|
|
278
315
|
[part='clear-highlight'] {
|
|
316
|
+
position: relative;
|
|
279
317
|
width: 18px;
|
|
280
318
|
height: 18px;
|
|
281
319
|
display: grid;
|
|
@@ -283,6 +321,18 @@ let MosaicaFilterPanel = (() => {
|
|
|
283
321
|
border-radius: 4px;
|
|
284
322
|
color: var(--_mosaica-ink-2);
|
|
285
323
|
}
|
|
324
|
+
/* Each takes a press from 24 px around its middle, drawn no larger for it. */
|
|
325
|
+
[part='clear-highlight']::after,
|
|
326
|
+
.chip .edit::after,
|
|
327
|
+
[part='clear']::after {
|
|
328
|
+
content: '';
|
|
329
|
+
position: absolute;
|
|
330
|
+
inset: -3px;
|
|
331
|
+
}
|
|
332
|
+
.chip .edit::after,
|
|
333
|
+
[part='clear']::after {
|
|
334
|
+
inset: -4px 0;
|
|
335
|
+
}
|
|
286
336
|
[part='clear-highlight']:hover {
|
|
287
337
|
background: var(--_mosaica-surface-3);
|
|
288
338
|
}
|
|
@@ -295,17 +345,30 @@ let MosaicaFilterPanel = (() => {
|
|
|
295
345
|
border-bottom: 1px solid var(--_mosaica-line-2);
|
|
296
346
|
}
|
|
297
347
|
[part='chip'] {
|
|
348
|
+
position: relative;
|
|
298
349
|
min-height: 0;
|
|
299
350
|
padding: 2px 4px 2px 8px;
|
|
300
351
|
gap: 4px;
|
|
301
352
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
353
|
+
/* A region held as the viewer leaves its view arrives here from the map's corner. */
|
|
354
|
+
[part='chip'][data-view] {
|
|
355
|
+
animation: arrive 1.2s ease-out 1;
|
|
356
|
+
}
|
|
357
|
+
@keyframes arrive {
|
|
358
|
+
from {
|
|
359
|
+
box-shadow: 0 0 0 3px var(--_mosaica-accent);
|
|
360
|
+
}
|
|
361
|
+
to {
|
|
362
|
+
box-shadow: 0 0 0 0 transparent;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
@media (prefers-reduced-motion: reduce) {
|
|
366
|
+
[part='chip'][data-view] {
|
|
367
|
+
animation: none;
|
|
368
|
+
}
|
|
307
369
|
}
|
|
308
370
|
.chip .edit {
|
|
371
|
+
position: relative;
|
|
309
372
|
display: inline-flex;
|
|
310
373
|
flex: 0 1 auto;
|
|
311
374
|
align-items: center;
|
|
@@ -313,7 +376,12 @@ let MosaicaFilterPanel = (() => {
|
|
|
313
376
|
font: inherit;
|
|
314
377
|
text-align: left;
|
|
315
378
|
}
|
|
379
|
+
/* The pen on a highlight's chip, which says in a shape what the chip's colour says. */
|
|
380
|
+
[part='chip'] > svg {
|
|
381
|
+
flex: none;
|
|
382
|
+
}
|
|
316
383
|
[part='clear'] {
|
|
384
|
+
position: relative;
|
|
317
385
|
margin-left: auto;
|
|
318
386
|
padding: 0;
|
|
319
387
|
font-size: 12px;
|
|
@@ -389,12 +457,37 @@ let MosaicaFilterPanel = (() => {
|
|
|
389
457
|
font-size: 12px;
|
|
390
458
|
color: var(--_mosaica-ink-3);
|
|
391
459
|
}
|
|
460
|
+
.emptied {
|
|
461
|
+
padding: 2px 4px;
|
|
462
|
+
border-radius: 4px;
|
|
463
|
+
font-size: 12px;
|
|
464
|
+
color: var(--_mosaica-ink-2);
|
|
465
|
+
}
|
|
466
|
+
@media (forced-colors: active) {
|
|
467
|
+
.swatch {
|
|
468
|
+
background: CanvasText;
|
|
469
|
+
}
|
|
470
|
+
.swatch.lit {
|
|
471
|
+
background: Highlight;
|
|
472
|
+
}
|
|
473
|
+
.swatch.pale {
|
|
474
|
+
background: GrayText;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
392
477
|
`
|
|
393
478
|
];
|
|
394
479
|
/** Every item the filters admit, which All matching counts. */
|
|
395
480
|
matching = (__runInitializers(this, _addActive_extraInitializers), new HeldAggregate('fields-match'));
|
|
396
481
|
/** The card to open, scroll to and focus once the cards are drawn. */
|
|
397
482
|
editing = null;
|
|
483
|
+
/** A pressed control that its own action removes, and the control that takes focus once it has gone. */
|
|
484
|
+
leaving = null;
|
|
485
|
+
#emptied_accessor_storage = __runInitializers(this, _emptied_initializers, false);
|
|
486
|
+
/** Whether the last chip was removed from a panel with nothing else to focus, which then says so. @internal */
|
|
487
|
+
get emptied() { return this.#emptied_accessor_storage; }
|
|
488
|
+
set emptied(value) { this.#emptied_accessor_storage = value; }
|
|
489
|
+
/** Whether those words held focus as a render began. */
|
|
490
|
+
wordsHeld = (__runInitializers(this, _emptied_extraInitializers), false);
|
|
398
491
|
/** The keys of the fields shown, in the order they were first shown. */
|
|
399
492
|
listOrder = [];
|
|
400
493
|
floating = new FloatingList(() => {
|
|
@@ -424,8 +517,41 @@ let MosaicaFilterPanel = (() => {
|
|
|
424
517
|
disconnectedCallback() {
|
|
425
518
|
this.floating.stop();
|
|
426
519
|
this.matching.set(null, null);
|
|
520
|
+
document.removeEventListener('pointerdown', this.onPressOutsideAdd, true);
|
|
427
521
|
super.disconnectedCallback();
|
|
428
522
|
}
|
|
523
|
+
/** A press outside the open Add field list closes it, as Escape does. */
|
|
524
|
+
onPressOutsideAdd = (e) => {
|
|
525
|
+
const adder = this.renderRoot.querySelector('.adder');
|
|
526
|
+
if (!adder || !e.composedPath().includes(adder))
|
|
527
|
+
this.adding = false;
|
|
528
|
+
};
|
|
529
|
+
/**
|
|
530
|
+
* Before an action that removes the pressed control `gone`, note where focus goes once it has.
|
|
531
|
+
* From a chip's ×, to the × of the chip after it, else of the chip before, as each stands then.
|
|
532
|
+
* From there, and from Clear all and the clear-highlight ×, to Add field: a × that took focus
|
|
533
|
+
* unasked would remove a filter on the next press. Under `chips-only`, which has no Add field, to
|
|
534
|
+
* the words that say no filter is left.
|
|
535
|
+
*/
|
|
536
|
+
leave(gone) {
|
|
537
|
+
const closes = Array.from(this.renderRoot.querySelectorAll('[part="chip"] > button:last-child'));
|
|
538
|
+
const at = closes.indexOf(gone);
|
|
539
|
+
this.leaving = { gone, to: at < 0 ? null : (closes[at + 1] ?? closes[at - 1] ?? null) };
|
|
540
|
+
}
|
|
541
|
+
/** Move focus as {@link leave} noted, once the pressed control has gone and taken focus with it. */
|
|
542
|
+
settleFocus() {
|
|
543
|
+
const leaving = this.leaving;
|
|
544
|
+
if (!leaving || leaving.gone.isConnected)
|
|
545
|
+
return;
|
|
546
|
+
this.leaving = null;
|
|
547
|
+
if (!focusLost())
|
|
548
|
+
return;
|
|
549
|
+
const to = (leaving.to?.isConnected ? leaving.to : null) ?? this.renderRoot.querySelector('[part="add"]');
|
|
550
|
+
if (to)
|
|
551
|
+
return to.focus();
|
|
552
|
+
this.emptied = true;
|
|
553
|
+
void this.updateComplete.then(() => this.renderRoot.querySelector('.emptied')?.focus());
|
|
554
|
+
}
|
|
429
555
|
/** Show `field`'s card, open it, and scroll to and focus it. Call it after `mosaica-chipopen` to show the chip's card. */
|
|
430
556
|
show(field) {
|
|
431
557
|
this.opened = new Set([...this.opened, field]);
|
|
@@ -478,6 +604,20 @@ let MosaicaFilterPanel = (() => {
|
|
|
478
604
|
updated(changed) {
|
|
479
605
|
super.updated(changed);
|
|
480
606
|
this.floating.update();
|
|
607
|
+
this.settleFocus();
|
|
608
|
+
// A chip that arrives takes the words' place, and their focus where they held it.
|
|
609
|
+
const arrived = this.emptied ? this.renderRoot.querySelector('[part="chip"] > button:last-child') : null;
|
|
610
|
+
if (arrived) {
|
|
611
|
+
this.emptied = false;
|
|
612
|
+
if (this.wordsHeld && focusLost())
|
|
613
|
+
arrived.focus();
|
|
614
|
+
}
|
|
615
|
+
if (changed.has('adding')) {
|
|
616
|
+
if (this.adding)
|
|
617
|
+
document.addEventListener('pointerdown', this.onPressOutsideAdd, true);
|
|
618
|
+
else
|
|
619
|
+
document.removeEventListener('pointerdown', this.onPressOutsideAdd, true);
|
|
620
|
+
}
|
|
481
621
|
const s = this.resolvedStore;
|
|
482
622
|
this.matching.set(s, this.isConnected && !this.chipsOnly && !this.controlsOnly && s?.get('meta') ? { groupings: [{ count: true }] } : null);
|
|
483
623
|
if (!this.editing || this.chipsOnly)
|
|
@@ -501,9 +641,12 @@ let MosaicaFilterPanel = (() => {
|
|
|
501
641
|
case 'category':
|
|
502
642
|
return `${caption}: ${draft.keys.map((k) => keyTitle(this.resolvedStore, column, k)).join(', ')}`;
|
|
503
643
|
case 'numeric': {
|
|
504
|
-
const
|
|
505
|
-
if (
|
|
644
|
+
const type = this.resolvedStore?.get('meta')?.declaredScalars.find((c) => c.name === column)?.arrowType;
|
|
645
|
+
if (type === 'timestamp_us')
|
|
506
646
|
return `${caption} ${dateRangeText(draft.gte, draft.lte)}`;
|
|
647
|
+
const kept = type === 'bool' ? boolKeys(draft) : [];
|
|
648
|
+
if (kept.length > 0)
|
|
649
|
+
return `${caption}: ${kept.map((k) => BOOL_WORDS[k]).join(', ')}`;
|
|
507
650
|
const f = (v) => v.toLocaleString('en-GB');
|
|
508
651
|
if (draft.lte === null)
|
|
509
652
|
return `${caption} ≥ ${f(draft.gte)}`;
|
|
@@ -515,14 +658,27 @@ let MosaicaFilterPanel = (() => {
|
|
|
515
658
|
}
|
|
516
659
|
/**
|
|
517
660
|
* A `member_of` chip's text: the clause's own label (the only name a filter layer's artifact has,
|
|
518
|
-
* since it is not served), else the served name, else unnamed.
|
|
661
|
+
* since it is not served), else the served name or key, else unnamed.
|
|
519
662
|
*/
|
|
520
663
|
memberText(clause) {
|
|
521
664
|
const artifacts = this.resolvedStore?.get('artifacts');
|
|
522
665
|
const served = artifacts?.served.find((a) => a.mosaicaId === clause.artifact && a.layer === clause.layer);
|
|
523
|
-
const name = clause.label ?? (served ? artifactName(served, artifacts.attached) : null)
|
|
666
|
+
const name = clause.label ?? called(served ? artifactName(served, artifacts.attached) : null, served?.key).text;
|
|
524
667
|
return clause.outside ? `Outside ${name}` : name;
|
|
525
668
|
}
|
|
669
|
+
/** Switch to the view a held region is in, where the store selects it again. */
|
|
670
|
+
showRegion(meta, view) {
|
|
671
|
+
const s = this.resolvedStore;
|
|
672
|
+
if (s)
|
|
673
|
+
switchView(this, s, meta, view);
|
|
674
|
+
}
|
|
675
|
+
clearRegion(view) {
|
|
676
|
+
const s = this.resolvedStore;
|
|
677
|
+
if (!s)
|
|
678
|
+
return;
|
|
679
|
+
s.setRegions(s.get('filters').regions.filter((r) => r.view !== view));
|
|
680
|
+
emit(this, 'mosaica-selectchange', { shape: null, view, status: 'cleared' });
|
|
681
|
+
}
|
|
526
682
|
clearMember(clause) {
|
|
527
683
|
const s = this.resolvedStore;
|
|
528
684
|
if (!s)
|
|
@@ -541,9 +697,13 @@ let MosaicaFilterPanel = (() => {
|
|
|
541
697
|
const meta = s?.get('meta');
|
|
542
698
|
if (!s || !meta)
|
|
543
699
|
return;
|
|
700
|
+
const held = s.get('filters').regions;
|
|
544
701
|
s.setFilters(emptyDraft(meta.filterOperands));
|
|
545
702
|
s.setMembers([]);
|
|
703
|
+
s.setRegions([]);
|
|
546
704
|
emit(this, 'mosaica-filterchange', { column: null, expr: null });
|
|
705
|
+
for (const { view } of held)
|
|
706
|
+
emit(this, 'mosaica-selectchange', { shape: null, view, status: 'cleared' });
|
|
547
707
|
}
|
|
548
708
|
/** Empty every clause in the highlight position, so the cards count what is in view again. */
|
|
549
709
|
clearHighlight() {
|
|
@@ -616,12 +776,12 @@ let MosaicaFilterPanel = (() => {
|
|
|
616
776
|
const layers = clusterFieldLayers(meta, s.get('view').id);
|
|
617
777
|
if (operands.length === 0 && layers.length === 0)
|
|
618
778
|
return html `<div class="panel"><span part="state" data-state="empty">Nothing to filter</span></div>`;
|
|
619
|
-
const { draft, members } = s.get('filters');
|
|
779
|
+
const { draft, members, regions } = s.get('filters');
|
|
620
780
|
const holds = (column, verb) => {
|
|
621
781
|
const d = draft[verb][column];
|
|
622
782
|
return d !== undefined && isPopulated(d);
|
|
623
783
|
};
|
|
624
|
-
const chips = this.chipLine(draft, members, holds);
|
|
784
|
+
const chips = this.chipLine(meta, draft, members, regions, holds);
|
|
625
785
|
if (this.chipsOnly)
|
|
626
786
|
return chips;
|
|
627
787
|
const fields = [
|
|
@@ -638,7 +798,7 @@ let MosaicaFilterPanel = (() => {
|
|
|
638
798
|
this.listOrder = [...this.listOrder.filter((k) => keys.has(k)), ...shown.map((f) => f.key).filter((k) => !this.listOrder.includes(k))];
|
|
639
799
|
const listed = this.listOrder.map((k) => shown.find((f) => f.key === k));
|
|
640
800
|
const open = this.compact ? (listed.some((f) => f.key === this.openCard) ? this.openCard : null) : null;
|
|
641
|
-
const card = (f) => html `<mosaica-field-card part="card" exportparts=${CARD_PARTS} data-field=${f.key} field=${f.key} .store=${s} .level=${this.
|
|
801
|
+
const card = (f) => html `<mosaica-field-card part="card" exportparts=${CARD_PARTS} data-field=${f.key} field=${f.key} ?hide-chips=${!this.controlsOnly} .store=${s} .level=${f.layer ? (this.clusterLevels.get(f.layer.name) ?? null) : null}
|
|
642
802
|
?compact=${this.compact} .folded=${this.compact ? f.key !== open : this.folded.has(f.key)}
|
|
643
803
|
@mosaica-fold=${(e) => this.onToggle(f.key, e.currentTarget)}
|
|
644
804
|
@mosaica-filterchange=${() => this.keepListed(f.key)} @mosaica-clausechange=${() => this.keepListed(f.key)}></mosaica-field-card>`;
|
|
@@ -655,26 +815,54 @@ let MosaicaFilterPanel = (() => {
|
|
|
655
815
|
const skel = html `<span class="skel" aria-hidden="true"></span>`;
|
|
656
816
|
return html `<div part="subject">
|
|
657
817
|
<span class="key"><span part="subject-key" class=${`swatch${lit ? ' lit' : ''}`}></span><span part="subject-name" class=${lit ? 'lit' : ''}>${lit ? 'Highlighted' : 'In view'}</span><span part="subject-count">${n === null ? skel : figure(n, this.compact)}</span>${lit
|
|
658
|
-
? html `<button part="clear-highlight" type="button" aria-label="Clear highlight" title="Clear highlight" @click=${() =>
|
|
818
|
+
? html `<button part="clear-highlight" type="button" aria-label="Clear highlight" title="Clear highlight" @click=${(e) => {
|
|
819
|
+
this.leave(e.currentTarget);
|
|
820
|
+
this.clearHighlight();
|
|
821
|
+
}}>${icon('close', 11, 2.4)}</button>`
|
|
659
822
|
: nothing}</span>
|
|
660
823
|
<span part="all" class="key"><span class="swatch pale"></span><span>${this.compact ? 'All' : 'All matching'}</span><span part="all-count">${total === null ? skel : figure(total, this.compact)}</span></span>
|
|
661
824
|
</div>`;
|
|
662
825
|
}
|
|
663
826
|
/** Every clause as a chip, then Clear all; nothing while none is applied. */
|
|
664
|
-
chipLine(draft, members, holds) {
|
|
827
|
+
chipLine(meta, draft, members, regions, holds) {
|
|
665
828
|
const columns = [...new Set([...Object.keys(draft.filter), ...Object.keys(draft.highlight)])];
|
|
666
829
|
const chips = VERBS.flatMap((verb) => columns.flatMap((c) => (holds(c, verb) ? [{ c, verb, d: draft[verb][c] }] : [])));
|
|
667
|
-
if (chips.length === 0 && members.length === 0)
|
|
668
|
-
return nothing;
|
|
830
|
+
if (chips.length === 0 && members.length === 0 && regions.length === 0) {
|
|
831
|
+
return this.emptied && this.chipsOnly ? html `<div part="chips"><span class="emptied" tabindex="-1" @blur=${() => (this.emptied = false)}>No filters</span></div>` : nothing;
|
|
832
|
+
}
|
|
833
|
+
// The pen on a highlight's chip, with the word for a reader; a filter's has neither.
|
|
834
|
+
const word = (verb) => (verb === 'highlight' ? html `${icon('highlight', 12, 1.6)}<span class="sr-only">highlight </span>` : nothing);
|
|
835
|
+
const pressed = (e) => this.leave(e.currentTarget);
|
|
669
836
|
return html `<div part="chips">
|
|
670
|
-
${chips
|
|
671
|
-
|
|
672
|
-
><button type="button" aria-label=${`Remove the ${columnCaption(c)} ${verb}`} @click=${() =>
|
|
837
|
+
${repeat(chips, ({ c, verb }) => `${verb} ${c}`, ({ c, verb, d }) => html `<span part="chip" class="chip" data-verb=${verb} data-column=${c}
|
|
838
|
+
>${word(verb)}<button part="edit" class="edit" type="button" title="Show its card" @click=${() => this.edit(c, verb)}>${this.chipText(c, d)}</button
|
|
839
|
+
><button type="button" aria-label=${`Remove the ${columnCaption(c)} ${verb}`} @click=${(e) => {
|
|
840
|
+
pressed(e);
|
|
841
|
+
this.clearColumn(c, verb);
|
|
842
|
+
}}>${icon('close', 10, 2.4)}</button></span
|
|
673
843
|
>`)}
|
|
674
|
-
${[...members].sort((a, b) => VERBS.indexOf(a.verb) - VERBS.indexOf(b.verb)).
|
|
675
|
-
>${this.memberText(m)}<button type="button" aria-label=${`Remove ${this.memberText(m)}`} @click=${() =>
|
|
844
|
+
${repeat([...members].sort((a, b) => VERBS.indexOf(a.verb) - VERBS.indexOf(b.verb)), (m) => `${m.verb} ${m.layer} ${m.artifact}`, (m) => html `<span part="chip" class="chip" data-verb=${m.verb} data-artifact=${String(m.artifact)}
|
|
845
|
+
>${word(m.verb)}${this.memberText(m)}<button type="button" aria-label=${`Remove the ${this.memberText(m)} ${m.verb}`} @click=${(e) => {
|
|
846
|
+
pressed(e);
|
|
847
|
+
this.clearMember(m);
|
|
848
|
+
}}>${icon('close', 10, 2.4)}</button></span
|
|
676
849
|
>`)}
|
|
677
|
-
|
|
850
|
+
${repeat(regions, (r) => r.view, (r) => {
|
|
851
|
+
const view = viewTitle(meta, r.view);
|
|
852
|
+
const text = regionText(r.shape, view);
|
|
853
|
+
return html `<span part="chip" class="chip" data-verb="filter" data-view=${r.view}
|
|
854
|
+
><button part="edit" class="edit" type="button" title=${`Show in ${view}`} @click=${() => this.showRegion(meta, r.view)}
|
|
855
|
+
>${icon(r.shape.kind === 'lasso' ? 'lasso' : 'box', 12)}${text}</button
|
|
856
|
+
><button type="button" aria-label=${`Remove the ${text} filter`} @click=${(e) => {
|
|
857
|
+
pressed(e);
|
|
858
|
+
this.clearRegion(r.view);
|
|
859
|
+
}}>${icon('close', 10, 2.4)}</button></span
|
|
860
|
+
>`;
|
|
861
|
+
})}
|
|
862
|
+
<button part="clear" type="button" @click=${(e) => {
|
|
863
|
+
pressed(e);
|
|
864
|
+
this.clearAll();
|
|
865
|
+
}}>Clear all</button>
|
|
678
866
|
</div>`;
|
|
679
867
|
}
|
|
680
868
|
/** Add field, its list of every field, and how many are not listed. */
|
|
@@ -690,7 +878,12 @@ let MosaicaFilterPanel = (() => {
|
|
|
690
878
|
this.takeOff(key);
|
|
691
879
|
};
|
|
692
880
|
const rest = fields.length - listed.length;
|
|
693
|
-
return html `<div class="adder"
|
|
881
|
+
return html `<div class="adder" @focusout=${(e) => {
|
|
882
|
+
// Focus that goes to nothing, as on a press on the list's own padding or scrollbar, has not left.
|
|
883
|
+
const to = e.relatedTarget;
|
|
884
|
+
if (to && !e.currentTarget.contains(to))
|
|
885
|
+
this.adding = false;
|
|
886
|
+
}}>
|
|
694
887
|
<button part="add" class="btn" type="button" aria-expanded=${this.adding ? 'true' : 'false'} aria-controls="add-list"
|
|
695
888
|
@click=${() => {
|
|
696
889
|
this.adding = !this.adding;
|
|
@@ -720,6 +913,9 @@ let MosaicaFilterPanel = (() => {
|
|
|
720
913
|
if (unlisted)
|
|
721
914
|
this.add(unlisted.key);
|
|
722
915
|
}} /></div>
|
|
916
|
+
<div role="status">${offered.length > 0
|
|
917
|
+
? html `<span class="sr-only">${offered.length} ${offered.length === 1 ? 'field' : 'fields'} listed</span>`
|
|
918
|
+
: html `<span class="none">No field by that name</span>`}</div>
|
|
723
919
|
${offered.length > 0
|
|
724
920
|
? html `<div role="menu" aria-label="Fields">${offered.map((o, i) => {
|
|
725
921
|
const added = isListed(o.key);
|
|
@@ -729,13 +925,17 @@ let MosaicaFilterPanel = (() => {
|
|
|
729
925
|
aria-disabled=${kept ? 'true' : nothing} title=${kept ? 'Always shown here' : added ? 'Remove from the column' : nothing} tabindex=${i === tabbed ? '0' : '-1'}
|
|
730
926
|
@click=${() => choose(o.key)} @keydown=${(e) => this.addKeys(e, i)}><span>${o.title}</span>${added ? html `<span class="kind">Shown</span>` : o.layer ? html `<span class="kind">Clusters</span>` : nothing}</button>`;
|
|
731
927
|
})}</div>`
|
|
732
|
-
:
|
|
928
|
+
: nothing}
|
|
733
929
|
</div>`
|
|
734
930
|
: nothing}
|
|
735
931
|
</div>`;
|
|
736
932
|
}
|
|
737
933
|
willUpdate(changed) {
|
|
738
934
|
super.willUpdate(changed);
|
|
935
|
+
let at = this.emptied && this.isConnected ? document.activeElement : null;
|
|
936
|
+
while (at && at.getRootNode() !== this.shadowRoot)
|
|
937
|
+
at = at.shadowRoot?.activeElement ?? null;
|
|
938
|
+
this.wordsHeld = at?.classList.contains('emptied') ?? false;
|
|
739
939
|
// The search box takes focus as the list opens.
|
|
740
940
|
if (changed.has('adding') && this.adding)
|
|
741
941
|
void this.updateComplete.then(() => this.renderRoot.querySelector('[part="add-search"]')?.focus());
|
package/dist/filter.d.ts
CHANGED
|
@@ -14,11 +14,16 @@ export declare const OPERATOR_WORDS: Record<KeywordOperator, string>;
|
|
|
14
14
|
* - A text column takes words that an item must all contain. Double quotes match a phrase where the
|
|
15
15
|
* column supports phrases, and `OR` between terms matches either.
|
|
16
16
|
* - A category column suggests values as the viewer types. The arrow keys move through the
|
|
17
|
-
* suggestions,
|
|
17
|
+
* suggestions, as do Home and End once one is reached. Enter chooses or removes the suggestion
|
|
18
|
+
* they reached, and Escape clears what was typed. A line under the box says how many values are
|
|
19
|
+
* listed, or why none is, and that they are loading once that has taken a quarter of a second.
|
|
18
20
|
* - A keyword column has a menu for choosing how to match, by `contains`, `prefix` or `eq`.
|
|
19
21
|
*
|
|
20
22
|
* The box sets the filter 350 ms after the last keystroke, so that the map does not redraw for
|
|
21
|
-
* every letter.
|
|
23
|
+
* every letter. While the query that change started is still loading, and once it has run for a
|
|
24
|
+
* quarter of a second, a spinner takes the search icon's place. The spinner is not read out:
|
|
25
|
+
* `<mosaica-status>` is what announces a running query. If your code sets a text filter the box
|
|
26
|
+
* cannot express, the box shows it read-only.
|
|
22
27
|
*
|
|
23
28
|
* @summary A search box that filters the map by one column.
|
|
24
29
|
* @tagname mosaica-filter
|
|
@@ -26,11 +31,13 @@ export declare const OPERATOR_WORDS: Record<KeywordOperator, string>;
|
|
|
26
31
|
* @fires {CustomEvent<MosaicaEventDetails['mosaica-filterchange']>} mosaica-filterchange - The box
|
|
27
32
|
* changed the column's filter.
|
|
28
33
|
* @csspart entry - The box.
|
|
34
|
+
* @csspart spinner - The spinner in the search icon's place while the box's own query runs.
|
|
29
35
|
* @csspart mode - The keyword column's operator button, which opens its menu.
|
|
30
36
|
* @csspart operators - The menu of a keyword column's operators.
|
|
31
37
|
* @csspart operator - One operator in the menu, with `aria-checked`.
|
|
32
38
|
* @csspart values - The typeahead's suggestions.
|
|
33
|
-
* @csspart tick - One suggested value, with `aria-selected
|
|
39
|
+
* @csspart tick - One suggested value, with `aria-selected`, and `data-active` on the one the arrow
|
|
40
|
+
* keys reached.
|
|
34
41
|
* @csspart bar - A bar for a suggested value's share of the items counted.
|
|
35
42
|
* @csspart value-count - A suggested value's count.
|
|
36
43
|
* @csspart more - The note under the box when nothing matches, or when more values match than are
|
|
@@ -56,13 +63,18 @@ export declare class MosaicaFilter extends MosaicaElement {
|
|
|
56
63
|
accessor draft: ColumnDraft | null;
|
|
57
64
|
/** @internal */
|
|
58
65
|
accessor search: string;
|
|
59
|
-
/** The suggestion the arrow keys
|
|
66
|
+
/** The suggestion the arrow keys reached, by code; `null` before they reach one. @internal */
|
|
60
67
|
accessor activeCode: number | null;
|
|
61
68
|
/** Whether the category box has focus, which its suggestions show only while it does. @internal */
|
|
62
69
|
accessor focused: boolean;
|
|
63
70
|
/** Whether a keyword column's operator menu is open. @internal */
|
|
64
71
|
accessor operatorsOpen: boolean;
|
|
65
72
|
private sent;
|
|
73
|
+
/** Whether the query the store is running was started by this box. */
|
|
74
|
+
private asked;
|
|
75
|
+
private readonly query;
|
|
76
|
+
/** Whether the suggestions have been awaited long enough to say so. */
|
|
77
|
+
private readonly awaited;
|
|
66
78
|
private readonly floating;
|
|
67
79
|
private readonly operatorMenu;
|
|
68
80
|
private typing;
|
|
@@ -92,10 +104,16 @@ export declare class MosaicaFilter extends MosaicaElement {
|
|
|
92
104
|
/** Moves focus to the box. */
|
|
93
105
|
focus(options?: FocusOptions): void;
|
|
94
106
|
protected updated(changed: PropertyValues<this>): void;
|
|
107
|
+
private get querying();
|
|
108
|
+
/** Whether a category box holds text whose suggestions have neither landed nor been refused. */
|
|
109
|
+
private get awaiting();
|
|
110
|
+
/** The search icon, or a spinner while the query this box started has run long enough to name. */
|
|
111
|
+
private lead;
|
|
95
112
|
/** The box's draft: the one being edited, else the store's, else the client's empty one, which also sets a keyword's starting operator. */
|
|
96
113
|
private currentDraft;
|
|
97
114
|
private change;
|
|
98
115
|
render(): TemplateResult | typeof nothing;
|
|
116
|
+
/** The box's name: the column, and the placeholder the box shows. */
|
|
99
117
|
private label;
|
|
100
118
|
private text;
|
|
101
119
|
/** A keyword box, offering the operators the column publishes in a menu. */
|