@uwdata/mosaic-inputs 0.7.1 → 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uwdata/mosaic-inputs",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
4
4
  "description": "Mosaic input components.",
5
5
  "keywords": [
6
6
  "inputs",
@@ -20,14 +20,14 @@
20
20
  "scripts": {
21
21
  "prebuild": "rimraf dist && mkdir dist",
22
22
  "build": "node ../../esbuild.js mosaic-inputs",
23
- "lint": "eslint src test --ext .js",
23
+ "lint": "eslint src test",
24
24
  "test": "mocha 'test/**/*-test.js'",
25
25
  "prepublishOnly": "npm run test && npm run lint && npm run build"
26
26
  },
27
27
  "dependencies": {
28
- "@uwdata/mosaic-core": "^0.7.1",
29
- "@uwdata/mosaic-sql": "^0.7.0",
28
+ "@uwdata/mosaic-core": "^0.9.0",
29
+ "@uwdata/mosaic-sql": "^0.9.0",
30
30
  "isoformat": "^0.2.1"
31
31
  },
32
- "gitHead": "7e6f3ea9b3011ea2c9201c1aa16e8e5664621a4c"
32
+ "gitHead": "89bb9b0dfa747aed691eaeba35379525a6764c61"
33
33
  }
package/src/Menu.js CHANGED
@@ -1,5 +1,5 @@
1
- import { MosaicClient, isParam, isSelection } from '@uwdata/mosaic-core';
2
- import { Query, eq, literal } from '@uwdata/mosaic-sql';
1
+ import { MosaicClient, Param, isParam, isSelection, point } 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 => {
@@ -9,6 +9,33 @@ const isObject = v => {
9
9
  export const menu = options => input(Menu, options);
10
10
 
11
11
  export class Menu extends MosaicClient {
12
+ /**
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.
38
+ */
12
39
  constructor({
13
40
  element,
14
41
  filterBy,
@@ -18,36 +45,52 @@ export class Menu extends MosaicClient {
18
45
  format = x => x, // TODO
19
46
  options,
20
47
  value,
48
+ field = column,
21
49
  as
22
50
  } = {}) {
23
51
  super(filterBy);
24
52
  this.from = from;
25
53
  this.column = column;
26
- this.selection = as;
27
54
  this.format = format;
55
+ this.field = field;
56
+ const selection = this.selection = as;
28
57
 
29
58
  this.element = element ?? document.createElement('div');
30
59
  this.element.setAttribute('class', 'input');
31
- this.element.value = this;
60
+ Object.defineProperty(this.element, 'value', { value: this });
32
61
 
33
62
  const lab = document.createElement('label');
34
63
  lab.innerText = label || column;
35
64
  this.element.appendChild(lab);
36
65
 
37
66
  this.select = document.createElement('select');
67
+ this.element.appendChild(this.select);
68
+
69
+ // if provided, populate menu options
38
70
  if (options) {
39
71
  this.data = options.map(value => isObject(value) ? value : { value });
72
+ this.selectedValue(value ?? '');
40
73
  this.update();
41
74
  }
42
- value = value ?? this.selection?.value ?? this.data?.[0]?.value;
43
- if (this.selection?.value === undefined) this.publish(value);
44
- this.element.appendChild(this.select);
45
75
 
46
- if (this.selection) {
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
47
88
  this.select.addEventListener('input', () => {
48
89
  this.publish(this.selectedValue() ?? null);
49
90
  });
50
- if (!isSelection(this.selection)) {
91
+
92
+ // if bound to a scalar param, respond to value updates
93
+ if (isParam) {
51
94
  this.selection.addEventListener('value', value => {
52
95
  if (value !== this.select.value) {
53
96
  this.selectedValue(value);
@@ -76,14 +119,11 @@ export class Menu extends MosaicClient {
76
119
  }
77
120
 
78
121
  publish(value) {
79
- const { selection, column } = this;
122
+ const { selection, field } = this;
80
123
  if (isSelection(selection)) {
81
- selection.update({
82
- source: this,
83
- schema: { type: 'point' },
84
- value,
85
- predicate: (value !== '' && value !== undefined) ? eq(column, literal(value)) : null
86
- });
124
+ if (value === '') value = undefined; // 'All' option
125
+ const clause = point(field, value, { source: this });
126
+ selection.update(clause);
87
127
  } else if (isParam(selection)) {
88
128
  selection.update(value);
89
129
  }
@@ -101,12 +141,15 @@ export class Menu extends MosaicClient {
101
141
  }
102
142
 
103
143
  queryResult(data) {
144
+ // column option values, with an inserted 'All' value
104
145
  this.data = [{ value: '', label: 'All' }, ...data];
105
146
  return this;
106
147
  }
107
148
 
108
149
  update() {
109
- const { data, format, select } = this;
150
+ const { data, format, select, selection } = this;
151
+
152
+ // generate menu item options
110
153
  select.replaceChildren();
111
154
  for (const { value, label } of data) {
112
155
  const opt = document.createElement('option');
@@ -114,9 +157,15 @@ export class Menu extends MosaicClient {
114
157
  opt.innerText = label ?? format(value);
115
158
  this.select.appendChild(opt);
116
159
  }
117
- if (this.selection) {
118
- this.selectedValue(this.selection?.value ?? '');
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 ?? '');
119
167
  }
168
+
120
169
  return this;
121
170
  }
122
171
  }
package/src/Search.js CHANGED
@@ -1,15 +1,37 @@
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, match } 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 {
10
+ /**
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.
34
+ */
13
35
  constructor({
14
36
  element,
15
37
  filterBy,
@@ -17,6 +39,7 @@ export class Search extends MosaicClient {
17
39
  column,
18
40
  label,
19
41
  type = 'contains',
42
+ field = column,
20
43
  as
21
44
  } = {}) {
22
45
  super(filterBy);
@@ -25,10 +48,11 @@ export class Search extends MosaicClient {
25
48
  this.from = from;
26
49
  this.column = column;
27
50
  this.selection = as;
51
+ this.field = field;
28
52
 
29
53
  this.element = element ?? document.createElement('div');
30
54
  this.element.setAttribute('class', 'input');
31
- this.element.value = this;
55
+ Object.defineProperty(this.element, 'value', { value: this });
32
56
 
33
57
  if (label) {
34
58
  const lab = document.createElement('label');
@@ -62,14 +86,10 @@ export class Search extends MosaicClient {
62
86
  }
63
87
 
64
88
  publish(value) {
65
- const { selection, column, type } = this;
89
+ const { selection, field, type } = this;
66
90
  if (isSelection(selection)) {
67
- selection.update({
68
- source: this,
69
- schema: { type },
70
- value,
71
- predicate: value ? FUNCTIONS[type](column, literal(value)) : null
72
- });
91
+ const clause = match(field, value, { source: this, method: type });
92
+ selection.update(clause);
73
93
  } else if (isParam(selection)) {
74
94
  selection.update(value);
75
95
  }
package/src/Slider.js CHANGED
@@ -1,5 +1,5 @@
1
- import { MosaicClient, isParam, isSelection } from '@uwdata/mosaic-core';
2
- import { Query, eq, literal, max, min } from '@uwdata/mosaic-sql';
1
+ import { MosaicClient, Param, interval, isParam, isSelection, point } 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;
@@ -7,6 +7,36 @@ let _id = 0;
7
7
  export const slider = options => input(Slider, options);
8
8
 
9
9
  export class Slider extends MosaicClient {
10
+ /**
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.
39
+ */
10
40
  constructor({
11
41
  element,
12
42
  filterBy,
@@ -18,6 +48,8 @@ export class Slider extends MosaicClient {
18
48
  column,
19
49
  label = column,
20
50
  value = as?.value,
51
+ select = 'point',
52
+ field = column,
21
53
  width
22
54
  } = {}) {
23
55
  super(filterBy);
@@ -25,45 +57,59 @@ export class Slider extends MosaicClient {
25
57
  this.from = from;
26
58
  this.column = column || 'value';
27
59
  this.selection = as;
60
+ this.selectionType = select;
61
+ this.field = field;
28
62
  this.min = min;
29
63
  this.max = max;
30
64
  this.step = step;
31
65
 
32
66
  this.element = element || document.createElement('div');
33
67
  this.element.setAttribute('class', 'input');
34
- this.element.value = this;
68
+ Object.defineProperty(this.element, 'value', { value: this });
35
69
 
36
70
  if (label) {
37
- const lab = document.createElement('label');
38
- lab.setAttribute('for', this.id);
39
- lab.innerText = label;
40
- this.element.appendChild(lab);
71
+ const desc = document.createElement('label');
72
+ desc.setAttribute('for', this.id);
73
+ desc.innerText = label;
74
+ this.element.appendChild(desc);
41
75
  }
42
76
 
43
77
  this.slider = document.createElement('input');
44
78
  this.slider.setAttribute('id', this.id);
45
79
  this.slider.setAttribute('type', 'range');
46
80
  if (width != null) this.slider.style.width = `${+width}px`;
47
- if (min != null) this.slider.setAttribute('min', min);
48
- if (max != null) this.slider.setAttribute('max', max);
49
- 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
50
92
  if (value != null) {
51
- this.slider.setAttribute('value', value);
93
+ this.slider.setAttribute('value', `${value}`);
52
94
  if (this.selection?.value === undefined) this.publish(value);
53
95
  }
54
- this.element.appendChild(this.slider);
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
+ });
55
104
 
56
- if (this.selection) {
57
- this.slider.addEventListener('input', () => {
58
- this.publish(+this.slider.value);
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
+ }
59
112
  });
60
- if (!isSelection(this.selection)) {
61
- this.selection.addEventListener('value', value => {
62
- if (value !== +this.slider.value) {
63
- this.slider.value = value;
64
- }
65
- });
66
- }
67
113
  }
68
114
  }
69
115
 
@@ -78,21 +124,36 @@ export class Slider extends MosaicClient {
78
124
 
79
125
  queryResult(data) {
80
126
  const { min, max } = Array.from(data)[0];
81
- if (this.min == null) this.slider.setAttribute('min', min);
82
- if (this.max == null) this.slider.setAttribute('max', max);
83
- if (this.step == null) this.slider.setAttribute('step', (max - min) / 500);
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
+ }
84
139
  return this;
85
140
  }
86
141
 
87
142
  publish(value) {
88
- const { selection, column } = this;
143
+ const { field, selectionType, selection } = this;
89
144
  if (isSelection(selection)) {
90
- selection.update({
91
- source: this,
92
- schema: { type: 'point' },
93
- value,
94
- predicate: eq(column, literal(value))
95
- });
145
+ if (selectionType === 'interval') {
146
+ /** @type {[number, number]} */
147
+ const domain = [this.min ?? 0, value];
148
+ selection.update(interval(field, domain, {
149
+ source: this,
150
+ bin: 'ceil',
151
+ scale: { type: 'identity', domain },
152
+ pixelSize: this.step
153
+ }));
154
+ } else {
155
+ selection.update(point(field, value, { source: this }));
156
+ }
96
157
  } else if (isParam(this.selection)) {
97
158
  selection.update(value);
98
159
  }
package/src/Table.js CHANGED
@@ -8,6 +8,10 @@ let _id = -1;
8
8
  export const table = options => input(Table, options);
9
9
 
10
10
  export class Table extends MosaicClient {
11
+ /**
12
+ * Create a new Table instance.
13
+ * @param {object} options Options object
14
+ */
11
15
  constructor({
12
16
  element,
13
17
  filterBy,
@@ -38,7 +42,7 @@ export class Table extends MosaicClient {
38
42
 
39
43
  this.element = element || document.createElement('div');
40
44
  this.element.setAttribute('id', this.id);
41
- this.element.value = this;
45
+ Object.defineProperty(this.element, 'value', { value: this });
42
46
  if (typeof width === 'number') this.element.style.width = `${width}px`;
43
47
  if (maxWidth) this.element.style.maxWidth = `${maxWidth}px`;
44
48
  this.element.style.maxHeight = `${height}px`;
@@ -42,6 +42,9 @@ export function formatDate(date) {
42
42
 
43
43
  // Memoize the last-returned locale.
44
44
  export function localize(f) {
45
- let key = localize, value;
46
- return (locale = 'en') => locale === key ? value : (value = f(key = locale));
45
+ let key = null;
46
+ let value;
47
+ return (locale = 'en') => locale === key
48
+ ? value
49
+ : (value = f(key = locale));
47
50
  }