datavis-glide 4.0.0-PRE.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.
Files changed (62) hide show
  1. package/LICENSE +45 -0
  2. package/README.md +129 -0
  3. package/datavis.js +101 -0
  4. package/dist/wcdatavis.css +1957 -0
  5. package/dist/wcdatavis.min.js +1 -0
  6. package/global-jquery.js +4 -0
  7. package/ie-fixes.js +13 -0
  8. package/index.js +70 -0
  9. package/meteor.js +1 -0
  10. package/package.json +102 -0
  11. package/src/flags.js +6 -0
  12. package/src/graph.js +1079 -0
  13. package/src/graph_renderer.js +85 -0
  14. package/src/grid.js +2777 -0
  15. package/src/grid_control.js +1957 -0
  16. package/src/grid_filter.js +1073 -0
  17. package/src/grid_renderer.js +276 -0
  18. package/src/group_fun_win.js +121 -0
  19. package/src/lang/en-US.js +188 -0
  20. package/src/lang/es-MX.js +188 -0
  21. package/src/lang/fr-FR.js +188 -0
  22. package/src/lang/id-ID.js +188 -0
  23. package/src/lang/nl-NL.js +188 -0
  24. package/src/lang/pt-BR.js +188 -0
  25. package/src/lang/ru-RU.js +188 -0
  26. package/src/lang/th-TH.js +188 -0
  27. package/src/lang/vi-VN.js +188 -0
  28. package/src/lang/zh-Hans-CN.js +188 -0
  29. package/src/operations_palette.js +176 -0
  30. package/src/prefs_modules.js +132 -0
  31. package/src/reg/graph_renderer.js +17 -0
  32. package/src/renderers/graph/chartjs.js +457 -0
  33. package/src/renderers/graph/google.js +584 -0
  34. package/src/renderers/graph/jit.js +61 -0
  35. package/src/renderers/graph/svelte-gantt.js +168 -0
  36. package/src/renderers/grid/dummy.js +79 -0
  37. package/src/renderers/grid/handlebars.js +217 -0
  38. package/src/renderers/grid/squirrelly.js +215 -0
  39. package/src/renderers/grid/table/group_detail.js +1404 -0
  40. package/src/renderers/grid/table/group_summary.js +380 -0
  41. package/src/renderers/grid/table/pivot.js +915 -0
  42. package/src/renderers/grid/table/plain.js +1592 -0
  43. package/src/renderers/grid/table.js +2510 -0
  44. package/src/trans.js +101 -0
  45. package/src/ui/collapsible.js +234 -0
  46. package/src/ui/filters/date.js +283 -0
  47. package/src/ui/grid_filter.js +398 -0
  48. package/src/ui/popup_menu.js +224 -0
  49. package/src/ui/popup_window.js +572 -0
  50. package/src/ui/slider.js +156 -0
  51. package/src/ui/tabs.js +202 -0
  52. package/src/ui/templates.js +131 -0
  53. package/src/ui/toolbar.js +63 -0
  54. package/src/ui/toolbars/grid.js +873 -0
  55. package/src/ui/windows/col_config.js +341 -0
  56. package/src/ui/windows/debug.js +164 -0
  57. package/src/ui/windows/grid_table_opts.js +139 -0
  58. package/src/util/handlebars.js +158 -0
  59. package/src/util/jquery.js +630 -0
  60. package/src/util/misc.js +1058 -0
  61. package/src/util/squirrelly.js +155 -0
  62. package/wcdatavis.css +1601 -0
@@ -0,0 +1,398 @@
1
+ import _ from 'underscore';
2
+ import moment from 'moment';
3
+ import numeral from 'numeral';
4
+ import jQuery from 'jquery';
5
+ import { BigNumber } from 'bignumber.js';
6
+
7
+ import {
8
+ icon,
9
+ isFloat,
10
+ isInt,
11
+ makeSubclass,
12
+ mixinLogging,
13
+ toFloat,
14
+ toInt,
15
+ } from '../util/misc.js';
16
+ import { trans } from '../trans.js';
17
+
18
+ /**
19
+ * @file
20
+ * Contains the implementation of "grid filters" which are the dynamically applied filters that are
21
+ * shown in the filter control of a grid. They set the filter on the {@link View} associated with
22
+ * the {@link Grid}.
23
+ *
24
+ * If you're looking for the parameters that get sent to the {@link Source} then you want {@link
25
+ * source_param.js} instead.
26
+ *
27
+ * ## Classes
28
+ *
29
+ * - {@link GridFilter}
30
+ * - {@link StringTextboxGridFilter}
31
+ * - {@link StringDropdownGridFilterChosen}
32
+ * - {@link StringDropdownGridFilterSumo}
33
+ * - {@link NumberTextboxGridFilter}
34
+ * - {@link NumberCheckboxGridFilter}
35
+ * - {@link DateSingleGridFilter}
36
+ * - {@link DateRangeGridFilter}
37
+ * - {@link BooleanCheckboxGridFilter}
38
+ * - {@link GridFilterSet}
39
+ */
40
+
41
+ // GridFilter {{{1
42
+
43
+ // Superclass {{{2
44
+
45
+ // Constructor {{{3
46
+
47
+ /**
48
+ * Base class for all grid filter widgets.
49
+ *
50
+ * @class
51
+ *
52
+ * @property {string} field
53
+ *
54
+ * @property {GridFilterSet} gridFilterSet
55
+ *
56
+ * @property {object} opts
57
+ *
58
+ * @property {string} [opts.filterType]
59
+ *
60
+ * @property {string} [opts.filterButton]
61
+ * The button used to add a new filter.
62
+ *
63
+ * @property {string} [opts.noRemoveButton=false]
64
+ * If true, don't create a remove button to place next to the filter.
65
+ *
66
+ * @property {number} limit
67
+ *
68
+ * @property {boolean} applyImmediately
69
+ *
70
+ * @property {jQuery} div
71
+ *
72
+ * @property {jQuery} input
73
+ *
74
+ * @property {jQuery} removeBtn
75
+ *
76
+ * @property {string} id
77
+ *
78
+ */
79
+
80
+ var GridFilter = (function () {
81
+ var id = 0;
82
+
83
+ var genId = function () {
84
+ return 'GridFilter_' + id++;
85
+ };
86
+
87
+ return makeSubclass('GridFilter', Object, function (field, gridFilterSet, typeInfo, opts) {
88
+ var self = this;
89
+ var localRemoveButton;
90
+
91
+ self.id = genId();
92
+ self.field = field;
93
+ self.gridFilterSet = gridFilterSet;
94
+ self.typeInfo = typeInfo;
95
+ self.opts = opts;
96
+
97
+ self.limit = 0;
98
+ self.applyImmediately = false;
99
+ self.div = jQuery('<div>')
100
+ .addClass('wcdv_filter_control_filter');
101
+
102
+ if (self.opts.makeRemoveButton) {
103
+ self.removeBtn = self.makeRemoveBtn();
104
+ localRemoveButton = self.removeBtn;
105
+ }
106
+ else if (self.opts.removeButton) {
107
+ localRemoveButton = self.opts.removeButton;
108
+ }
109
+
110
+ if (localRemoveButton) {
111
+ localRemoveButton.on('click', function () {
112
+ self.gridFilterSet.remove(self.getId(), self.opts.filterButton);
113
+ if (typeof self.opts.onRemove === 'function') {
114
+ self.opts.onRemove();
115
+ }
116
+ });
117
+ }
118
+
119
+ if (self.gridFilterSet.gridTable) {
120
+ self.gridFilterSet.gridTable.on('columnResize', function () {
121
+ self.adjustInputWidth({ useSizingElement: true, fromColumnResize: true });
122
+ });
123
+ }
124
+ });
125
+ })();
126
+
127
+ mixinLogging(GridFilter);
128
+
129
+ // #getValue {{{3
130
+
131
+ /**
132
+ * This represents an exact value to use with a filter operator to decide what to show in the grid.
133
+ *
134
+ * @typedef {string|Moment|Numeral} GridFilter~Value
135
+ */
136
+
137
+ /**
138
+ * This represents a range of allowed values; anything within the range should be shown in the grid.
139
+ *
140
+ * @typedef {Object} GridFilter~RangeValue
141
+ *
142
+ * @property {GridFilter~Value} start The starting number / date in the range (inclusive).
143
+ * @property {GridFilter~Value} end The ending number / date in the range (inclusive).
144
+ */
145
+
146
+ /**
147
+ * Gives the value that should be used when building the filters for the View from the user's
148
+ * input in the GridFilter. A GridFilter can return either a single value (which should be combined
149
+ * with the operator, e.g. "greater than 40") or a range value (where the operators are implicitly
150
+ * greater-than-or-equal and less-than-or-equal, e.g. "between January 1st and March 31st").
151
+ *
152
+ * @returns {GridFilter~Value|GridFilter~RangeValue} The value of the filter; you can tell whether
153
+ * or not it will be a range by checking the result of #isRange().
154
+ */
155
+
156
+ GridFilter.prototype.getValue = function () {
157
+ var self = this
158
+ , fti = self.gridFilterSet.view.typeInfo.get(self.field);
159
+
160
+ switch (fti.type) {
161
+ case 'date':
162
+ case 'time':
163
+ case 'datetime':
164
+ if (self.input.val() === '') {
165
+ return undefined;
166
+ }
167
+ return fti.internalType === 'moment' ? moment(self.input.val()) : self.input.val();
168
+ case 'number':
169
+ case 'currency':
170
+ if (self.input.val() === '') {
171
+ return undefined;
172
+ }
173
+ switch (fti.internalType) {
174
+ case 'bignumber':
175
+ return new BigNumber(self.input.val());
176
+ case 'numeral':
177
+ return numeral(self.input.val());
178
+ case 'primitive':
179
+ return isInt(self.input.val()) ? toInt(self.input.val())
180
+ : isFloat(self.input.val()) ? toFloat(self.input.val())
181
+ : self.input.val();
182
+ default:
183
+ return self.input.val();
184
+ }
185
+ case 'string':
186
+ default:
187
+ return self.input.val();
188
+ }
189
+ };
190
+
191
+ // #setValue {{{3
192
+
193
+ GridFilter.prototype.setValue = function (val) {
194
+ var self = this;
195
+
196
+ if (['$exists', '$notexists'].indexOf(self.getOperator()) < 0) {
197
+ if (numeral && numeral.isNumeral(val)) {
198
+ self.input.val(val._value);
199
+ }
200
+ else {
201
+ self.input.val(val);
202
+ }
203
+ }
204
+ };
205
+
206
+ // #getOperator {{{3
207
+
208
+ GridFilter.prototype.getOperator = function () {
209
+ var self = this;
210
+
211
+ return self.operatorDrop.val();
212
+ };
213
+
214
+ // #setOperator {{{3
215
+
216
+ GridFilter.prototype.setOperator = function (op) {
217
+ var self = this;
218
+
219
+ if (self.operatorDrop) {
220
+ self.operatorDrop.val(op);
221
+ self.operatorDrop.change();
222
+ }
223
+ };
224
+
225
+ // #getId {{{3
226
+
227
+ GridFilter.prototype.getId = function () {
228
+ return this.id;
229
+ };
230
+
231
+ // #makeOperatorDrop {{{3
232
+
233
+ /**
234
+ * Construct a SELECT that allows the user to pick the operator.
235
+ *
236
+ * @param {Array<string>} include If present, only include operators that correspond to those
237
+ * operations requested. This should be an array like ``['$eq', '$ne']`` to only show equality and
238
+ * inequality operators.
239
+ */
240
+
241
+ GridFilter.prototype.makeOperatorDrop = function (include) {
242
+ var self = this;
243
+
244
+ // These are all the operators that are possible.
245
+
246
+ var operators = [
247
+ {value: '$contains', text: '∈'},
248
+ {value: '$notcontains', text: '∉'},
249
+ {value: '$eq', text: '='},
250
+ {value: '$ne', text: '≠'},
251
+ {value: '$gt', text: '>'},
252
+ {value: '$gte', text: '≥'},
253
+ {value: '$lt', text: '<'},
254
+ {value: '$lte', text: '≤'},
255
+ {value: '$in', text: 'in', transLabel: 'FILTER.STRING.OPERATOR.IN'},
256
+ {value: '$nin', text: 'not in', transLabel: 'FILTER.STRING.OPERATOR.NOT_IN'},
257
+ {value: '$exists', text: 'not blank', transLabel: 'FILTER.STRING.OPERATOR.NOT_BLANK'},
258
+ {value: '$notexists', text: 'blank', transLabel: 'FILTER.STRING.OPERATOR.BLANK'}
259
+ ];
260
+
261
+ // Remove anything that user didn't ask for.
262
+
263
+ if (include !== undefined && _.isArray(include)) {
264
+ operators = _.reject(operators, function (elt) {
265
+ return include.indexOf(elt.value) < 0;
266
+ });
267
+ }
268
+
269
+ var operatorDrop = jQuery('<select>');
270
+
271
+ operatorDrop.css({'margin-right': '0.5em'});
272
+
273
+ // Add all the operators as options within the <SELECT>.
274
+
275
+ _.each(operators, function (op) {
276
+ operatorDrop.append(jQuery('<option>', { value: op.value }).text(op.transLabel != null ? trans(op.transLabel) : op.text));
277
+ });
278
+
279
+ // Hook up the event to update the filter when the operator is changed.
280
+
281
+ operatorDrop.on('change', function () {
282
+ // Hide the input when selecting either the "blank" or "not blank" operator.
283
+ // Show the input when selecting any other operator.
284
+
285
+ if (self.input) {
286
+ ['$exists', '$notexists'].indexOf(self.getOperator()) >= 0
287
+ ? self.hideInput()
288
+ : self.showInput();
289
+ }
290
+
291
+ // For non-blank operators, only update the filter spec when the input has something in it.
292
+ if (['$exists', '$notexists'].indexOf(self.getOperator()) >= 0 || self.getValue() !== '') {
293
+ self.gridFilterSet.update();
294
+ }
295
+ });
296
+
297
+ // Return the <SELECT> so that the caller can put it where they want.
298
+
299
+ return operatorDrop;
300
+ };
301
+
302
+ // #makeRemoveBtn {{{3
303
+
304
+ GridFilter.prototype.makeRemoveBtn = function () {
305
+ var self = this;
306
+
307
+ var removeBtn = jQuery(icon('x', null, 'Click to remove filter'));
308
+
309
+ removeBtn.css({'cursor': 'pointer', 'margin-left': '0.5em'});
310
+ return removeBtn;
311
+ };
312
+
313
+ // #remove {{{3
314
+
315
+ GridFilter.prototype.remove = function () {
316
+ var self = this;
317
+
318
+ self.div.remove();
319
+ self.gridFilterSet.update(false);
320
+ };
321
+
322
+ // #isRange {{{3
323
+
324
+ GridFilter.prototype.isRange = function () {
325
+ return false;
326
+ };
327
+
328
+ // #adjustInputWidth {{{3
329
+
330
+ GridFilter.prototype.adjustInputWidth = function (opts) {
331
+ var self = this;
332
+
333
+ if (!self.opts.autoUpdateInputWidth) {
334
+ return;
335
+ }
336
+
337
+ if (opts === undefined) {
338
+ opts = {};
339
+ }
340
+
341
+ // In case we're using TableTool, we need to carry around this idea of the sizing element. At
342
+ // this point in the JS execution, TableTool hasn't caught up and correctly resized the floating
343
+ // header to match the original header column widths. Therefore, we can't use `self.div` for
344
+ // determining the correct width (it's the wrong size, because it's still in a column which is the
345
+ // wrong size). Instead, we need to use the sizing element - which is the original version of the
346
+ // TH containing `self.div` - to determine the correct size. TableTool will catch up later,
347
+ // correctly resizing the column to align perfectly with what we set here.
348
+ //
349
+ // FIXME: This is extremely tightly coupled to knowledge about how the grid table is laid out and
350
+ // what features it has (e.g. TableTool). It would be better to pass in what the size of the
351
+ // column currently is with the event handler.
352
+
353
+ _.defaults(opts, {
354
+ useSizingElement: false,
355
+ input: self.input
356
+ });
357
+
358
+ self.logDebug(self.makeLogTag() + ' Target: %O', opts.input);
359
+
360
+ var targetWidth = opts.useSizingElement ? self.opts.sizingElement.width() : self.div.width();
361
+ self.logDebug(self.makeLogTag() + ' Available Space: ' + targetWidth + 'px ' + (opts.useSizingElement ? '[sizing element]' : '[div]'));
362
+
363
+ if (self.removeBtn) {
364
+ targetWidth -= self.removeBtn.outerWidth();
365
+ self.logDebug(self.makeLogTag() + ' Remove Button: ' + self.removeBtn.outerWidth() + 'px');
366
+ }
367
+
368
+ if (self.operatorDrop !== undefined) {
369
+ targetWidth -= self.operatorDrop.outerWidth();
370
+ self.logDebug(self.makeLogTag() + ' Operator Drop: ' + self.operatorDrop.outerWidth() + 'px');
371
+ }
372
+
373
+ self.logDebug(self.makeLogTag() + ' Adjusting ' + self.field + ' filter widget width to ' + targetWidth + 'px to match column width');
374
+
375
+ opts.input.outerWidth(targetWidth);
376
+
377
+ if (typeof opts.callback === 'function') {
378
+ opts.callback(targetWidth);
379
+ }
380
+ };
381
+
382
+ // #showInput {{{3
383
+
384
+ GridFilter.prototype.showInput = function (input) {
385
+ var self = this;
386
+
387
+ self.input.show();
388
+ };
389
+
390
+ // #hideInput {{{3
391
+
392
+ GridFilter.prototype.hideInput = function (input) {
393
+ var self = this;
394
+
395
+ self.input.hide();
396
+ };
397
+
398
+ export default GridFilter;
@@ -0,0 +1,224 @@
1
+ import jQuery from 'jquery';
2
+ import {
3
+ icon,
4
+ makeSubclass,
5
+ } from '../util/misc.js';
6
+
7
+ // Constructor {{{1
8
+
9
+ /**
10
+ * Create a new popup menu instance. A popup menu displays a list of items, each with an icon and
11
+ * label, and invokes a callback when the user clicks on one.
12
+ *
13
+ * @class
14
+ * @property {Array} items
15
+ * @property {object} ui
16
+ * @property {HTMLElement|null} ui.root
17
+ */
18
+
19
+ var PopupMenu = makeSubclass('PopupMenu', Object, function () {
20
+ var self = this;
21
+
22
+ self.items = [];
23
+ self.ui = {
24
+ root: null
25
+ };
26
+ self._boundClose = null;
27
+ });
28
+
29
+ // #addItem {{{1
30
+
31
+ /**
32
+ * Add an item to the menu.
33
+ *
34
+ * @param {string} label
35
+ * The text to display for this item.
36
+ *
37
+ * @param {string} iconName
38
+ * A Lucide icon name (e.g. 'arrow-up-narrow-wide').
39
+ *
40
+ * @param {function} callback
41
+ * Called when the item is clicked. Receives `userdata` as its argument.
42
+ *
43
+ * @param {*} [userdata]
44
+ * Arbitrary data passed to the callback when this item is clicked.
45
+ */
46
+
47
+ PopupMenu.prototype.addItem = function (label, iconName, callback, userdata) {
48
+ var self = this;
49
+
50
+ self.items.push({
51
+ label: label,
52
+ iconName: iconName,
53
+ callback: callback,
54
+ userdata: userdata,
55
+ separator: false
56
+ });
57
+ };
58
+
59
+ // #addSeparator {{{1
60
+
61
+ /**
62
+ * Add a visual separator to the menu.
63
+ */
64
+
65
+ PopupMenu.prototype.addSeparator = function () {
66
+ var self = this;
67
+
68
+ self.items.push({
69
+ separator: true
70
+ });
71
+ };
72
+
73
+ // #open {{{1
74
+
75
+ /**
76
+ * Open the popup menu. If an anchor element is provided, the menu is positioned near it, taking
77
+ * care to remain within the browser viewport.
78
+ *
79
+ * @param {HTMLElement|jQuery} [anchorElement]
80
+ * Optional element to position the menu near.
81
+ */
82
+
83
+ PopupMenu.prototype.open = function (anchorElement) {
84
+ var self = this;
85
+
86
+ // Close any previously open instance of this menu.
87
+ self.close();
88
+
89
+ var root = document.createElement('div');
90
+ root.className = 'wcdv-popup-menu';
91
+ root.setAttribute('role', 'menu');
92
+
93
+ for (var i = 0; i < self.items.length; i++) {
94
+ var entry = self.items[i];
95
+
96
+ if (entry.separator) {
97
+ var sep = document.createElement('div');
98
+ sep.className = 'wcdv-popup-menu-sep';
99
+ sep.setAttribute('role', 'separator');
100
+ root.appendChild(sep);
101
+ continue;
102
+ }
103
+
104
+ var item = document.createElement('div');
105
+ item.className = 'wcdv-popup-menu-item';
106
+ item.setAttribute('role', 'menuitem');
107
+
108
+ if (entry.iconName) {
109
+ var iconElt = icon(entry.iconName).get(0);
110
+ if (iconElt) {
111
+ item.appendChild(iconElt);
112
+ }
113
+ }
114
+
115
+ var labelSpan = document.createElement('span');
116
+ labelSpan.className = 'wcdv-popup-menu-item-label';
117
+ labelSpan.textContent = entry.label || '';
118
+ item.appendChild(labelSpan);
119
+
120
+ // Bind the click handler using an IIFE to capture the current entry.
121
+ (function (e) {
122
+ item.addEventListener('click', function (evt) {
123
+ evt.stopPropagation();
124
+ self.close();
125
+ if (typeof e.callback === 'function') {
126
+ e.callback(e.userdata);
127
+ }
128
+ });
129
+ })(entry);
130
+
131
+ root.appendChild(item);
132
+ }
133
+
134
+ document.body.appendChild(root);
135
+ self.ui.root = root;
136
+
137
+ // Position the menu near the anchor element, clamped to the viewport.
138
+ if (anchorElement) {
139
+ var anchor = anchorElement instanceof jQuery ? anchorElement.get(0) : anchorElement;
140
+ var rect = anchor.getBoundingClientRect();
141
+
142
+ // Start below-left of the anchor.
143
+ var top = rect.bottom;
144
+ var left = rect.left;
145
+
146
+ // Measure the menu now that it's in the DOM.
147
+ var menuRect = root.getBoundingClientRect();
148
+ var vpWidth = window.innerWidth;
149
+ var vpHeight = window.innerHeight;
150
+
151
+ // Clamp horizontally.
152
+ if (left + menuRect.width > vpWidth) {
153
+ left = vpWidth - menuRect.width;
154
+ }
155
+ if (left < 0) {
156
+ left = 0;
157
+ }
158
+
159
+ // If the menu would overflow below, try placing it above the anchor.
160
+ if (top + menuRect.height > vpHeight) {
161
+ var above = rect.top - menuRect.height;
162
+ if (above >= 0) {
163
+ top = above;
164
+ }
165
+ else {
166
+ // Neither fits perfectly; pick whichever side has more room.
167
+ top = rect.top > (vpHeight - rect.bottom)
168
+ ? Math.max(0, rect.top - menuRect.height)
169
+ : rect.bottom;
170
+ }
171
+ }
172
+
173
+ root.style.position = 'fixed';
174
+ root.style.top = top + 'px';
175
+ root.style.left = left + 'px';
176
+ }
177
+
178
+ // Close when the user clicks outside the menu.
179
+ self._boundClose = function (evt) {
180
+ if (self.ui.root && !self.ui.root.contains(evt.target)) {
181
+ self.close();
182
+ }
183
+ };
184
+
185
+ // Use a timeout so the current click event doesn't immediately close the menu.
186
+ setTimeout(function () {
187
+ document.addEventListener('mousedown', self._boundClose, true);
188
+ }, 0);
189
+ };
190
+
191
+ // #close {{{1
192
+
193
+ /**
194
+ * Close the popup menu, removing it from the page.
195
+ */
196
+
197
+ PopupMenu.prototype.close = function () {
198
+ var self = this;
199
+
200
+ if (self._boundClose) {
201
+ document.removeEventListener('mousedown', self._boundClose, true);
202
+ self._boundClose = null;
203
+ }
204
+
205
+ if (self.ui.root && self.ui.root.parentNode) {
206
+ self.ui.root.parentNode.removeChild(self.ui.root);
207
+ }
208
+ self.ui.root = null;
209
+ };
210
+
211
+ // #destroy {{{1
212
+
213
+ /**
214
+ * Destroy the popup menu, releasing all resources.
215
+ */
216
+
217
+ PopupMenu.prototype.destroy = function () {
218
+ var self = this;
219
+
220
+ self.close();
221
+ self.items = [];
222
+ };
223
+
224
+ export default PopupMenu;