@uwdata/mosaic-inputs 0.8.0 → 0.10.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/dist/mosaic-inputs.js +1220 -550
- package/dist/mosaic-inputs.min.js +6 -6
- package/package.json +5 -5
- package/src/Menu.js +66 -21
- package/src/Search.js +31 -15
- package/src/Slider.js +91 -34
- package/src/Table.js +53 -10
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uwdata/mosaic-inputs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Mosaic input components.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"inputs",
|
|
7
7
|
"mosaic"
|
|
8
8
|
],
|
|
9
9
|
"license": "BSD-3-Clause",
|
|
10
|
-
"author": "Jeffrey Heer (
|
|
10
|
+
"author": "Jeffrey Heer (https://idl.uw.edu)",
|
|
11
11
|
"type": "module",
|
|
12
12
|
"main": "src/index.js",
|
|
13
13
|
"module": "src/index.js",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"prepublishOnly": "npm run test && npm run lint && npm run build"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@uwdata/mosaic-core": "^0.
|
|
29
|
-
"@uwdata/mosaic-sql": "^0.
|
|
28
|
+
"@uwdata/mosaic-core": "^0.10.0",
|
|
29
|
+
"@uwdata/mosaic-sql": "^0.10.0",
|
|
30
30
|
"isoformat": "^0.2.1"
|
|
31
31
|
},
|
|
32
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "94fc4f0d4efc622001f6afd6714d1e9dda745be2"
|
|
33
33
|
}
|
package/src/Menu.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { MosaicClient, isParam, isSelection } from '@uwdata/mosaic-core';
|
|
2
|
-
import { Query
|
|
1
|
+
import { MosaicClient, Param, isParam, isSelection, clausePoint } from '@uwdata/mosaic-core';
|
|
2
|
+
import { Query } from '@uwdata/mosaic-sql';
|
|
3
3
|
import { input } from './input.js';
|
|
4
4
|
|
|
5
5
|
const isObject = v => {
|
|
@@ -10,8 +10,31 @@ export const menu = options => input(Menu, options);
|
|
|
10
10
|
|
|
11
11
|
export class Menu extends MosaicClient {
|
|
12
12
|
/**
|
|
13
|
-
* Create a new
|
|
14
|
-
* @param {object} options Options object
|
|
13
|
+
* Create a new menu input.
|
|
14
|
+
* @param {object} [options] Options object
|
|
15
|
+
* @param {HTMLElement} [options.element] The parent DOM element in which to
|
|
16
|
+
* place the menu elements. If undefined, a new `div` element is created.
|
|
17
|
+
* @param {Selection} [options.filterBy] A selection to filter the database
|
|
18
|
+
* table indicated by the *from* option.
|
|
19
|
+
* @param {Param} [options.as] The output param or selection. A selection
|
|
20
|
+
* clause is added for the currently selected menu option.
|
|
21
|
+
* @param {string} [options.field] The database column name to use within
|
|
22
|
+
* generated selection clause predicates. Defaults to the *column* option.
|
|
23
|
+
* @param {(any | { value: any, label?: string })[]} [options.options] An
|
|
24
|
+
* array of menu options, as literal values or option objects. Option
|
|
25
|
+
* objects have a `value` property and an optional `label` property. If no
|
|
26
|
+
* label or *format* function is provided, the string-coerced value is used.
|
|
27
|
+
* @param {(value: any) => string} [options.format] A format function that
|
|
28
|
+
* takes an option value as input and generates a string label. The format
|
|
29
|
+
* function is not applied when an explicit label is provided in an option
|
|
30
|
+
* object.
|
|
31
|
+
* @param {*} [options.value] The initial selected menu value.
|
|
32
|
+
* @param {string} [options.from] The name of a database table to use as a data
|
|
33
|
+
* source for this widget. Used in conjunction with the *column* option.
|
|
34
|
+
* @param {string} [options.column] The name of a database column from which
|
|
35
|
+
* to pull menu options. The unique column values are used as menu options.
|
|
36
|
+
* Used in conjunction with the *from* option.
|
|
37
|
+
* @param {string} [options.label] A text label for this input.
|
|
15
38
|
*/
|
|
16
39
|
constructor({
|
|
17
40
|
element,
|
|
@@ -22,36 +45,52 @@ export class Menu extends MosaicClient {
|
|
|
22
45
|
format = x => x, // TODO
|
|
23
46
|
options,
|
|
24
47
|
value,
|
|
48
|
+
field = column,
|
|
25
49
|
as
|
|
26
50
|
} = {}) {
|
|
27
51
|
super(filterBy);
|
|
28
52
|
this.from = from;
|
|
29
53
|
this.column = column;
|
|
30
|
-
this.selection = as;
|
|
31
54
|
this.format = format;
|
|
55
|
+
this.field = field;
|
|
56
|
+
const selection = this.selection = as;
|
|
32
57
|
|
|
33
58
|
this.element = element ?? document.createElement('div');
|
|
34
59
|
this.element.setAttribute('class', 'input');
|
|
35
|
-
this.element
|
|
60
|
+
Object.defineProperty(this.element, 'value', { value: this });
|
|
36
61
|
|
|
37
62
|
const lab = document.createElement('label');
|
|
38
63
|
lab.innerText = label || column;
|
|
39
64
|
this.element.appendChild(lab);
|
|
40
65
|
|
|
41
66
|
this.select = document.createElement('select');
|
|
67
|
+
this.element.appendChild(this.select);
|
|
68
|
+
|
|
69
|
+
// if provided, populate menu options
|
|
42
70
|
if (options) {
|
|
43
71
|
this.data = options.map(value => isObject(value) ? value : { value });
|
|
72
|
+
this.selectedValue(value ?? '');
|
|
44
73
|
this.update();
|
|
45
74
|
}
|
|
46
|
-
value = value ?? this.selection?.value ?? this.data?.[0]?.value;
|
|
47
|
-
if (this.selection?.value === undefined) this.publish(value);
|
|
48
|
-
this.element.appendChild(this.select);
|
|
49
75
|
|
|
50
|
-
|
|
76
|
+
// initialize selection or param bindings
|
|
77
|
+
if (selection) {
|
|
78
|
+
const isParam = !isSelection(selection);
|
|
79
|
+
|
|
80
|
+
// publish any initial menu value to the selection/param
|
|
81
|
+
// later updates propagate this back to the menu element
|
|
82
|
+
// do not publish if using a param that already has a value
|
|
83
|
+
if (value != null && (!isParam || selection.value === undefined)) {
|
|
84
|
+
this.publish(value);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// publish selected value upon menu change
|
|
51
88
|
this.select.addEventListener('input', () => {
|
|
52
89
|
this.publish(this.selectedValue() ?? null);
|
|
53
90
|
});
|
|
54
|
-
|
|
91
|
+
|
|
92
|
+
// if bound to a scalar param, respond to value updates
|
|
93
|
+
if (isParam) {
|
|
55
94
|
this.selection.addEventListener('value', value => {
|
|
56
95
|
if (value !== this.select.value) {
|
|
57
96
|
this.selectedValue(value);
|
|
@@ -80,14 +119,11 @@ export class Menu extends MosaicClient {
|
|
|
80
119
|
}
|
|
81
120
|
|
|
82
121
|
publish(value) {
|
|
83
|
-
const { selection,
|
|
122
|
+
const { selection, field } = this;
|
|
84
123
|
if (isSelection(selection)) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
value,
|
|
89
|
-
predicate: (value !== '' && value !== undefined) ? eq(column, literal(value)) : null
|
|
90
|
-
});
|
|
124
|
+
if (value === '') value = undefined; // 'All' option
|
|
125
|
+
const clause = clausePoint(field, value, { source: this });
|
|
126
|
+
selection.update(clause);
|
|
91
127
|
} else if (isParam(selection)) {
|
|
92
128
|
selection.update(value);
|
|
93
129
|
}
|
|
@@ -105,12 +141,15 @@ export class Menu extends MosaicClient {
|
|
|
105
141
|
}
|
|
106
142
|
|
|
107
143
|
queryResult(data) {
|
|
144
|
+
// column option values, with an inserted 'All' value
|
|
108
145
|
this.data = [{ value: '', label: 'All' }, ...data];
|
|
109
146
|
return this;
|
|
110
147
|
}
|
|
111
148
|
|
|
112
149
|
update() {
|
|
113
|
-
const { data, format, select } = this;
|
|
150
|
+
const { data, format, select, selection } = this;
|
|
151
|
+
|
|
152
|
+
// generate menu item options
|
|
114
153
|
select.replaceChildren();
|
|
115
154
|
for (const { value, label } of data) {
|
|
116
155
|
const opt = document.createElement('option');
|
|
@@ -118,9 +157,15 @@ export class Menu extends MosaicClient {
|
|
|
118
157
|
opt.innerText = label ?? format(value);
|
|
119
158
|
this.select.appendChild(opt);
|
|
120
159
|
}
|
|
121
|
-
|
|
122
|
-
|
|
160
|
+
|
|
161
|
+
// update menu value based on param/selection
|
|
162
|
+
if (selection) {
|
|
163
|
+
const value = isSelection(selection)
|
|
164
|
+
? selection.valueFor(this)
|
|
165
|
+
: selection.value;
|
|
166
|
+
this.selectedValue(value ?? '');
|
|
123
167
|
}
|
|
168
|
+
|
|
124
169
|
return this;
|
|
125
170
|
}
|
|
126
171
|
}
|
package/src/Search.js
CHANGED
|
@@ -1,18 +1,36 @@
|
|
|
1
|
-
import { MosaicClient, isParam, isSelection } from '@uwdata/mosaic-core';
|
|
2
|
-
import {
|
|
3
|
-
Query, regexp_matches, contains, prefix, suffix, literal
|
|
4
|
-
} from '@uwdata/mosaic-sql';
|
|
1
|
+
import { MosaicClient, Param, isParam, isSelection, clauseMatch } from '@uwdata/mosaic-core';
|
|
2
|
+
import { Query } from '@uwdata/mosaic-sql';
|
|
5
3
|
import { input } from './input.js';
|
|
6
4
|
|
|
7
|
-
const FUNCTIONS = { contains, prefix, suffix, regexp: regexp_matches };
|
|
8
5
|
let _id = 0;
|
|
9
6
|
|
|
10
7
|
export const search = options => input(Search, options);
|
|
11
8
|
|
|
12
9
|
export class Search extends MosaicClient {
|
|
13
10
|
/**
|
|
14
|
-
* Create a new
|
|
15
|
-
* @param {object} options Options object
|
|
11
|
+
* Create a new text search input.
|
|
12
|
+
* @param {object} [options] Options object
|
|
13
|
+
* @param {HTMLElement} [options.element] The parent DOM element in which to
|
|
14
|
+
* place the search elements. If undefined, a new `div` element is created.
|
|
15
|
+
* @param {Selection} [options.filterBy] A selection to filter the database
|
|
16
|
+
* table indicated by the *from* option.
|
|
17
|
+
* @param {Param} [options.as] The output param or selection. A selection
|
|
18
|
+
* clause is added based on the current text search query.
|
|
19
|
+
* @param {string} [options.field] The database column name to use within
|
|
20
|
+
* generated selection clause predicates. Defaults to the *column* option.
|
|
21
|
+
* @param {'contains' | 'prefix' | 'suffix' | 'regexp'} [options.type] The
|
|
22
|
+
* type of text search query to perform. One of:
|
|
23
|
+
* - `"contains"` (default): the query string may appear anywhere in the text
|
|
24
|
+
* - `"prefix"`: the query string must appear at the start of the text
|
|
25
|
+
* - `"suffix"`: the query string must appear at the end of the text
|
|
26
|
+
* - `"regexp"`: the query string is a regular expression the text must match
|
|
27
|
+
* @param {string} [options.from] The name of a database table to use as an
|
|
28
|
+
* autocomplete data source for this widget. Used in conjunction with the
|
|
29
|
+
* *column* option.
|
|
30
|
+
* @param {string} [options.column] The name of a database column from which
|
|
31
|
+
* to pull valid search results. The unique column values are used as search
|
|
32
|
+
* autocomplete values. Used in conjunction with the *from* option.
|
|
33
|
+
* @param {string} [options.label] A text label for this input.
|
|
16
34
|
*/
|
|
17
35
|
constructor({
|
|
18
36
|
element,
|
|
@@ -21,6 +39,7 @@ export class Search extends MosaicClient {
|
|
|
21
39
|
column,
|
|
22
40
|
label,
|
|
23
41
|
type = 'contains',
|
|
42
|
+
field = column,
|
|
24
43
|
as
|
|
25
44
|
} = {}) {
|
|
26
45
|
super(filterBy);
|
|
@@ -29,10 +48,11 @@ export class Search extends MosaicClient {
|
|
|
29
48
|
this.from = from;
|
|
30
49
|
this.column = column;
|
|
31
50
|
this.selection = as;
|
|
51
|
+
this.field = field;
|
|
32
52
|
|
|
33
53
|
this.element = element ?? document.createElement('div');
|
|
34
54
|
this.element.setAttribute('class', 'input');
|
|
35
|
-
this.element
|
|
55
|
+
Object.defineProperty(this.element, 'value', { value: this });
|
|
36
56
|
|
|
37
57
|
if (label) {
|
|
38
58
|
const lab = document.createElement('label');
|
|
@@ -66,14 +86,10 @@ export class Search extends MosaicClient {
|
|
|
66
86
|
}
|
|
67
87
|
|
|
68
88
|
publish(value) {
|
|
69
|
-
const { selection,
|
|
89
|
+
const { selection, field, type } = this;
|
|
70
90
|
if (isSelection(selection)) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
schema: { type },
|
|
74
|
-
value,
|
|
75
|
-
predicate: value ? FUNCTIONS[type](column, literal(value)) : null
|
|
76
|
-
});
|
|
91
|
+
const clause = clauseMatch(field, value, { source: this, method: type });
|
|
92
|
+
selection.update(clause);
|
|
77
93
|
} else if (isParam(selection)) {
|
|
78
94
|
selection.update(value);
|
|
79
95
|
}
|
package/src/Slider.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { MosaicClient, isParam, isSelection } from '@uwdata/mosaic-core';
|
|
2
|
-
import { Query,
|
|
1
|
+
import { MosaicClient, Param, clauseInterval, clausePoint, isParam, isSelection } from '@uwdata/mosaic-core';
|
|
2
|
+
import { Query, max, min } from '@uwdata/mosaic-sql';
|
|
3
3
|
import { input } from './input.js';
|
|
4
4
|
|
|
5
5
|
let _id = 0;
|
|
@@ -8,8 +8,34 @@ export const slider = options => input(Slider, options);
|
|
|
8
8
|
|
|
9
9
|
export class Slider extends MosaicClient {
|
|
10
10
|
/**
|
|
11
|
-
* Create a new
|
|
12
|
-
* @param {object} options Options object
|
|
11
|
+
* Create a new slider input.
|
|
12
|
+
* @param {object} [options] Options object
|
|
13
|
+
* @param {HTMLElement} [options.element] The parent DOM element in which to
|
|
14
|
+
* place the slider elements. If undefined, a new `div` element is created.
|
|
15
|
+
* @param {Selection} [options.filterBy] A selection to filter the database
|
|
16
|
+
* table indicated by the *from* option.
|
|
17
|
+
* @param {Param} [options.as] The output param or selection. A selection
|
|
18
|
+
* clause is added based on the currently selected slider option.
|
|
19
|
+
* @param {string} [options.field] The database column name to use within
|
|
20
|
+
* generated selection clause predicates. Defaults to the *column* option.
|
|
21
|
+
* @param {'point' | 'interval'} [options.select] The type of selection clause
|
|
22
|
+
* predicate to generate if the **as** option is a Selection. If `'point'`
|
|
23
|
+
* (the default), the selection predicate is an equality check for the slider
|
|
24
|
+
* value. If `'interval'`, the predicate checks an interval from the minimum
|
|
25
|
+
* to the current slider value.
|
|
26
|
+
* @param {number} [options.min] The minimum slider value.
|
|
27
|
+
* @param {number} [options.max] The maximum slider value.
|
|
28
|
+
* @param {number} [options.step] The slider step, the amount to increment
|
|
29
|
+
* between consecutive values.
|
|
30
|
+
* @param {number} [options.value] The initial slider value.
|
|
31
|
+
* @param {string} [options.from] The name of a database table to use as a data
|
|
32
|
+
* source for this widget. Used in conjunction with the *column* option.
|
|
33
|
+
* The minimum and maximum values of the column determine the slider range.
|
|
34
|
+
* @param {string} [options.column] The name of a database column whose values
|
|
35
|
+
* determine the slider range. Used in conjunction with the *from* option.
|
|
36
|
+
* The minimum and maximum values of the column determine the slider range.
|
|
37
|
+
* @param {string} [options.label] A text label for this input.
|
|
38
|
+
* @param {number} [options.width] The width of the slider in screen pixels.
|
|
13
39
|
*/
|
|
14
40
|
constructor({
|
|
15
41
|
element,
|
|
@@ -22,6 +48,8 @@ export class Slider extends MosaicClient {
|
|
|
22
48
|
column,
|
|
23
49
|
label = column,
|
|
24
50
|
value = as?.value,
|
|
51
|
+
select = 'point',
|
|
52
|
+
field = column,
|
|
25
53
|
width
|
|
26
54
|
} = {}) {
|
|
27
55
|
super(filterBy);
|
|
@@ -29,45 +57,59 @@ export class Slider extends MosaicClient {
|
|
|
29
57
|
this.from = from;
|
|
30
58
|
this.column = column || 'value';
|
|
31
59
|
this.selection = as;
|
|
60
|
+
this.selectionType = select;
|
|
61
|
+
this.field = field;
|
|
32
62
|
this.min = min;
|
|
33
63
|
this.max = max;
|
|
34
64
|
this.step = step;
|
|
35
65
|
|
|
36
66
|
this.element = element || document.createElement('div');
|
|
37
67
|
this.element.setAttribute('class', 'input');
|
|
38
|
-
this.element
|
|
68
|
+
Object.defineProperty(this.element, 'value', { value: this });
|
|
39
69
|
|
|
40
70
|
if (label) {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
this.element.appendChild(
|
|
71
|
+
const desc = document.createElement('label');
|
|
72
|
+
desc.setAttribute('for', this.id);
|
|
73
|
+
desc.innerText = label;
|
|
74
|
+
this.element.appendChild(desc);
|
|
45
75
|
}
|
|
46
76
|
|
|
47
77
|
this.slider = document.createElement('input');
|
|
48
78
|
this.slider.setAttribute('id', this.id);
|
|
49
79
|
this.slider.setAttribute('type', 'range');
|
|
50
80
|
if (width != null) this.slider.style.width = `${+width}px`;
|
|
51
|
-
if (min != null) this.slider.setAttribute('min', min);
|
|
52
|
-
if (max != null) this.slider.setAttribute('max', max);
|
|
53
|
-
if (step != null) this.slider.setAttribute('step', step);
|
|
81
|
+
if (min != null) this.slider.setAttribute('min', `${min}`);
|
|
82
|
+
if (max != null) this.slider.setAttribute('max', `${max}`);
|
|
83
|
+
if (step != null) this.slider.setAttribute('step', `${step}`);
|
|
84
|
+
this.element.appendChild(this.slider);
|
|
85
|
+
|
|
86
|
+
this.curval = document.createElement('label');
|
|
87
|
+
this.curval.setAttribute('for', this.id);
|
|
88
|
+
this.curval.setAttribute('class', 'value');
|
|
89
|
+
this.element.appendChild(this.curval);
|
|
90
|
+
|
|
91
|
+
// handle initial value
|
|
54
92
|
if (value != null) {
|
|
55
|
-
this.slider.setAttribute('value', value);
|
|
93
|
+
this.slider.setAttribute('value', `${value}`);
|
|
56
94
|
if (this.selection?.value === undefined) this.publish(value);
|
|
57
95
|
}
|
|
58
|
-
this.
|
|
96
|
+
this.curval.innerText = this.slider.value;
|
|
97
|
+
|
|
98
|
+
// respond to slider input
|
|
99
|
+
this.slider.addEventListener('input', () => {
|
|
100
|
+
const { value } = this.slider;
|
|
101
|
+
this.curval.innerText = value;
|
|
102
|
+
if (this.selection) this.publish(+value);
|
|
103
|
+
});
|
|
59
104
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
105
|
+
// track param updates
|
|
106
|
+
if (this.selection && !isSelection(this.selection)) {
|
|
107
|
+
this.selection.addEventListener('value', value => {
|
|
108
|
+
if (value !== +this.slider.value) {
|
|
109
|
+
this.slider.value = value;
|
|
110
|
+
this.curval.innerText = value;
|
|
111
|
+
}
|
|
63
112
|
});
|
|
64
|
-
if (!isSelection(this.selection)) {
|
|
65
|
-
this.selection.addEventListener('value', value => {
|
|
66
|
-
if (value !== +this.slider.value) {
|
|
67
|
-
this.slider.value = value;
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
113
|
}
|
|
72
114
|
}
|
|
73
115
|
|
|
@@ -82,21 +124,36 @@ export class Slider extends MosaicClient {
|
|
|
82
124
|
|
|
83
125
|
queryResult(data) {
|
|
84
126
|
const { min, max } = Array.from(data)[0];
|
|
85
|
-
if (this.min == null)
|
|
86
|
-
|
|
87
|
-
|
|
127
|
+
if (this.min == null) {
|
|
128
|
+
this.min = min;
|
|
129
|
+
this.slider.setAttribute('min', `${min}`);
|
|
130
|
+
}
|
|
131
|
+
if (this.max == null) {
|
|
132
|
+
this.max = max;
|
|
133
|
+
this.slider.setAttribute('max', `${max}`);
|
|
134
|
+
}
|
|
135
|
+
if (this.step == null) {
|
|
136
|
+
this.step = (max - min) / 500;
|
|
137
|
+
this.slider.setAttribute('step', `${this.step}`);
|
|
138
|
+
}
|
|
88
139
|
return this;
|
|
89
140
|
}
|
|
90
141
|
|
|
91
142
|
publish(value) {
|
|
92
|
-
const {
|
|
143
|
+
const { field, selectionType, selection } = this;
|
|
93
144
|
if (isSelection(selection)) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
145
|
+
if (selectionType === 'interval') {
|
|
146
|
+
/** @type {[number, number]} */
|
|
147
|
+
const domain = [this.min ?? 0, value];
|
|
148
|
+
selection.update(clauseInterval(field, domain, {
|
|
149
|
+
source: this,
|
|
150
|
+
bin: 'ceil',
|
|
151
|
+
scale: { type: 'identity', domain },
|
|
152
|
+
pixelSize: this.step
|
|
153
|
+
}));
|
|
154
|
+
} else {
|
|
155
|
+
selection.update(clausePoint(field, value, { source: this }));
|
|
156
|
+
}
|
|
100
157
|
} else if (isParam(this.selection)) {
|
|
101
158
|
selection.update(value);
|
|
102
159
|
}
|
package/src/Table.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MosaicClient, coordinator } from '@uwdata/mosaic-core';
|
|
1
|
+
import { MosaicClient, clausePoints, coordinator, toDataColumns } from '@uwdata/mosaic-core';
|
|
2
2
|
import { Query, column, desc } from '@uwdata/mosaic-sql';
|
|
3
3
|
import { formatDate, formatLocaleAuto, formatLocaleNumber } from './util/format.js';
|
|
4
4
|
import { input } from './input.js';
|
|
@@ -23,6 +23,7 @@ export class Table extends MosaicClient {
|
|
|
23
23
|
maxWidth,
|
|
24
24
|
height = 500,
|
|
25
25
|
rowBatch = 100,
|
|
26
|
+
as
|
|
26
27
|
} = {}) {
|
|
27
28
|
super(filterBy);
|
|
28
29
|
this.id = `table-${++_id}`;
|
|
@@ -36,13 +37,16 @@ export class Table extends MosaicClient {
|
|
|
36
37
|
this.limit = +rowBatch;
|
|
37
38
|
this.pending = false;
|
|
38
39
|
|
|
40
|
+
this.selection = as;
|
|
41
|
+
this.currentRow = -1;
|
|
42
|
+
|
|
39
43
|
this.sortHeader = null;
|
|
40
44
|
this.sortColumn = null;
|
|
41
45
|
this.sortDesc = false;
|
|
42
46
|
|
|
43
47
|
this.element = element || document.createElement('div');
|
|
44
48
|
this.element.setAttribute('id', this.id);
|
|
45
|
-
this.element
|
|
49
|
+
Object.defineProperty(this.element, 'value', { value: this });
|
|
46
50
|
if (typeof width === 'number') this.element.style.width = `${width}px`;
|
|
47
51
|
if (maxWidth) this.element.style.maxWidth = `${maxWidth}px`;
|
|
48
52
|
this.element.style.maxHeight = `${height}px`;
|
|
@@ -72,10 +76,34 @@ export class Table extends MosaicClient {
|
|
|
72
76
|
this.body = document.createElement('tbody');
|
|
73
77
|
this.tbl.appendChild(this.body);
|
|
74
78
|
|
|
79
|
+
if (this.selection) {
|
|
80
|
+
this.body.addEventListener('pointerover', evt => {
|
|
81
|
+
const row = resolveRow(evt.target);
|
|
82
|
+
if (row > -1 && row !== this.currentRow) {
|
|
83
|
+
this.currentRow = row;
|
|
84
|
+
this.selection.update(this.clause([row]));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
this.body.addEventListener('pointerleave', () => {
|
|
88
|
+
this.currentRow = -1;
|
|
89
|
+
this.selection.update(this.clause());
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
75
93
|
this.style = document.createElement('style');
|
|
76
94
|
this.element.appendChild(this.style);
|
|
77
95
|
}
|
|
78
96
|
|
|
97
|
+
clause(rows = []) {
|
|
98
|
+
const { data, limit, schema } = this;
|
|
99
|
+
const fields = schema.map(s => s.column);
|
|
100
|
+
const values = rows.map(row => {
|
|
101
|
+
const { columns } = data[~~(row / limit)];
|
|
102
|
+
return fields.map(f => columns[f][row % limit]);
|
|
103
|
+
});
|
|
104
|
+
return clausePoints(fields, values, { source: this });
|
|
105
|
+
}
|
|
106
|
+
|
|
79
107
|
requestData(offset = 0) {
|
|
80
108
|
this.offset = offset;
|
|
81
109
|
|
|
@@ -133,30 +161,35 @@ export class Table extends MosaicClient {
|
|
|
133
161
|
if (!this.pending) {
|
|
134
162
|
// data is not from an internal request, so reset table
|
|
135
163
|
this.loaded = false;
|
|
164
|
+
this.data = [];
|
|
136
165
|
this.body.replaceChildren();
|
|
166
|
+
this.offset = 0;
|
|
137
167
|
}
|
|
138
|
-
this.data
|
|
168
|
+
this.data.push(toDataColumns(data));
|
|
139
169
|
return this;
|
|
140
170
|
}
|
|
141
171
|
|
|
142
172
|
update() {
|
|
143
173
|
const { body, formats, data, schema, limit } = this;
|
|
144
174
|
const nf = schema.length;
|
|
175
|
+
const n = data.length - 1;
|
|
176
|
+
const rowCount = limit * n;
|
|
145
177
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
178
|
+
const { numRows, columns } = data[n];
|
|
179
|
+
const cols = schema.map(s => columns[s.column]);
|
|
180
|
+
for (let i = 0; i < numRows; ++i) {
|
|
149
181
|
const tr = document.createElement('tr');
|
|
150
|
-
|
|
151
|
-
|
|
182
|
+
Object.assign(tr, { __row__: rowCount + i });
|
|
183
|
+
for (let j = 0; j < nf; ++j) {
|
|
184
|
+
const value = cols[j][i];
|
|
152
185
|
const td = document.createElement('td');
|
|
153
|
-
td.innerText = value == null ? '' : formats[
|
|
186
|
+
td.innerText = value == null ? '' : formats[j](value);
|
|
154
187
|
tr.appendChild(td);
|
|
155
188
|
}
|
|
156
189
|
body.appendChild(tr);
|
|
157
190
|
}
|
|
158
191
|
|
|
159
|
-
if (
|
|
192
|
+
if (numRows < limit) {
|
|
160
193
|
// data table has been fully loaded
|
|
161
194
|
this.loaded = true;
|
|
162
195
|
}
|
|
@@ -190,6 +223,16 @@ export class Table extends MosaicClient {
|
|
|
190
223
|
}
|
|
191
224
|
}
|
|
192
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Resolve a table row number from a table cell element.
|
|
228
|
+
* @param {any} element An HTML element.
|
|
229
|
+
* @returns {number} The resolved row, or -1 if not a row.
|
|
230
|
+
*/
|
|
231
|
+
function resolveRow(element) {
|
|
232
|
+
const p = element.parentElement;
|
|
233
|
+
return Object.hasOwn(p, '__row__') ? +p.__row__ : -1;
|
|
234
|
+
}
|
|
235
|
+
|
|
193
236
|
function formatof(base = {}, schema, locale) {
|
|
194
237
|
return schema.map(({ column, type }) => {
|
|
195
238
|
if (column in base) {
|