datavis-glide 4.0.2 → 4.1.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.
@@ -185,6 +185,7 @@ var GridTable = makeSubclass('GridTable', GridRenderer, function () {
185
185
  self.super['GridRenderer'].ctor.apply(self, arguments);
186
186
 
187
187
  self.selection = [];
188
+ self.cellSelection = null;
188
189
  self.needsRedraw = false;
189
190
  self.popupMenus = [];
190
191
  self.csvLock = new Lock('GridTable/csv');
@@ -275,6 +276,7 @@ mixinEventHandling(GridTable, [
275
276
  , 'renderBegin'
276
277
  , 'renderEnd'
277
278
  , 'selectionChange'
279
+ , 'cellSelectionChange'
278
280
  ]);
279
281
 
280
282
  // #_validateFeatures {{{2
@@ -818,9 +820,13 @@ GridTable.prototype._addSortingToHeader = function (data, orientation, spec, con
818
820
  *
819
821
  * @param {string} [dir]
820
822
  * What direction we're sorting by, ascending or descending.
823
+ *
824
+ * @param {number} [priority]
825
+ * When this column is part of a multi-column sort, its 1-based position in the sort chain. When
826
+ * given, a numbered badge is shown next to the direction arrow.
821
827
  */
822
828
 
823
- var replaceSortIndicator = function (span, dir) {
829
+ var replaceSortIndicator = function (span, dir, priority) {
824
830
  if (!(span instanceof Element)) {
825
831
  throw new Error('Call Error: `span` must be an Element');
826
832
  }
@@ -832,6 +838,9 @@ GridTable.prototype._addSortingToHeader = function (data, orientation, spec, con
832
838
  throw new Error('Call Error: `dir` must be either "ASC" or "DESC"');
833
839
  }
834
840
  }
841
+ if (priority != null && !_.isNumber(priority)) {
842
+ throw new Error('Call Error: `priority` must be null or a number');
843
+ }
835
844
 
836
845
  var th = container.closest('th');
837
846
 
@@ -846,15 +855,72 @@ GridTable.prototype._addSortingToHeader = function (data, orientation, spec, con
846
855
  iconName = dir.toUpperCase() === 'ASC' ? 'arrow-up' : 'arrow-down';
847
856
  }
848
857
 
858
+ // Clear any prior content (icon and/or priority badge) before rebuilding.
859
+ while (span.firstChild) {
860
+ span.removeChild(span.firstChild);
861
+ }
862
+
849
863
  var newIcon = createLucideSvg(iconName);
850
864
  if (newIcon) {
851
865
  newIcon.classList.add('wcdv_icon');
852
866
  newIcon.setAttribute('data-icon', iconName);
853
- while (span.firstChild) {
854
- span.removeChild(span.firstChild);
855
- }
856
867
  span.appendChild(newIcon);
857
868
  }
869
+
870
+ // Show a numbered badge when this column is one of several in a multi-column sort.
871
+ if (dir != null && priority != null) {
872
+ var badge = document.createElement('span');
873
+ badge.className = 'wcdv_sort_priority_badge';
874
+ badge.textContent = String(priority);
875
+ badge.setAttribute('aria-label', trans('GRID.TABLE.SORT_MENU.PRIORITY_BADGE', priority));
876
+ span.appendChild(badge);
877
+ }
878
+ };
879
+
880
+ /**
881
+ * Compare two sort specs to see if they refer to the same column or aggregate, ignoring sort
882
+ * direction. Used to avoid adding duplicate entries to a sort chain.
883
+ *
884
+ * @param {object} a
885
+ * @param {object} b
886
+ * @returns {boolean}
887
+ */
888
+
889
+ var sortEntriesMatch = function (a, b) {
890
+ return a.field === b.field
891
+ && a.groupFieldIndex === b.groupFieldIndex
892
+ && a.pivotFieldIndex === b.pivotFieldIndex
893
+ && a.aggType === b.aggType
894
+ && a.aggNum === b.aggNum
895
+ ;
896
+ };
897
+
898
+ /**
899
+ * Determine whether a sort entry applies to the current output mode (plain, grouped, or
900
+ * pivotted). The view keeps a single sort chain per orientation, but each output mode produces
901
+ * its own kinds of sortable headers, so an entry created in one mode must not count towards the
902
+ * sort in another. This keeps each mode's sort independent.
903
+ *
904
+ * @param {object} entry
905
+ * A sort chain entry.
906
+ *
907
+ * @returns {boolean}
908
+ */
909
+
910
+ var sortEntryAppliesTo = function (entry) {
911
+ if (entry.pivotFieldIndex != null || entry.aggType === 'pivot' || entry.colVal != null || entry.rowVal != null) {
912
+ return !!data.isPivot;
913
+ }
914
+ if (entry.aggType === 'group') {
915
+ return !!data.isGroup;
916
+ }
917
+ if (entry.groupFieldIndex != null) {
918
+ return !!(data.isGroup || data.isPivot);
919
+ }
920
+ if (entry.field != null) {
921
+ return !!data.isPlain;
922
+ }
923
+ return true;
858
924
  };
859
925
 
860
926
  /**
@@ -866,9 +932,13 @@ GridTable.prototype._addSortingToHeader = function (data, orientation, spec, con
866
932
  * @param {number} [aggNum]
867
933
  * If missing, no aggregate number is added to the sort spec. Used when sorting directly by the
868
934
  * field (e.g. in plain output) or by the group field index (e.g. in group detail output).
935
+ *
936
+ * @param {boolean} [additive]
937
+ * When true, this column is appended to the existing sort chain (or its direction updated if it's
938
+ * already in the chain) rather than replacing it. Used by the per-row "add to sort" button.
869
939
  */
870
940
 
871
- var setSort = function (dir, aggNum) {
941
+ var setSort = function (dir, aggNum, additive) {
872
942
  if (!_.isString(dir)) {
873
943
  throw new Error('Call Error: `dir` must be a string');
874
944
  }
@@ -880,19 +950,64 @@ GridTable.prototype._addSortingToHeader = function (data, orientation, spec, con
880
950
  throw new Error('Call Error: `aggNum` must be a number');
881
951
  }
882
952
 
883
- jQuery('button.wcdv_icon_button' + sortIcon_orientationClass + '.wcdv_sort_icon').each(function (i, elt) {
884
- replaceSortIndicator(elt);
885
- });
886
-
887
- jQuery('button.wcdv_icon_button.' + sortIcon_class).each(function (i, elt) {
888
- replaceSortIndicator(elt, dir);
889
- });
890
-
891
953
  spec.aggNum = aggNum;
892
954
  spec.dir = dir;
893
955
 
894
- var sortSpec = self.view.getSort() || {};
895
- sortSpec[orientation] = deepCopy(spec);
956
+ // Deep-copy the view's current sort so we build a brand-new spec object. `getSort()` returns
957
+ // the view's live internal object; mutating it in place would defeat the view's change
958
+ // detection (it compares the new spec to its current one), which would silently skip saving
959
+ // the updated sort to the perspective.
960
+ var sortSpec = deepCopy(self.view.getSort()) || {};
961
+
962
+ // The view stores each orientation's sort as a chain (array) of specs. Older specs may be a
963
+ // bare object, so normalize to an array before working with it.
964
+ var chain = _.isArray(sortSpec[orientation])
965
+ ? deepCopy(sortSpec[orientation])
966
+ : sortSpec[orientation] != null
967
+ ? [deepCopy(sortSpec[orientation])]
968
+ : []
969
+ ;
970
+
971
+ if (additive) {
972
+
973
+ // Add this column to the existing chain. If it's already present, just update its
974
+ // direction so we never create duplicate entries for the same column.
975
+
976
+ var matchIndex = -1;
977
+ for (var ci = 0; ci < chain.length; ci++) {
978
+ if (sortEntriesMatch(chain[ci], spec)) {
979
+ matchIndex = ci;
980
+ break;
981
+ }
982
+ }
983
+ if (matchIndex < 0) {
984
+ chain.push(deepCopy(spec));
985
+ }
986
+ else {
987
+ chain[matchIndex].dir = dir;
988
+ }
989
+ }
990
+ else {
991
+
992
+ // Replace this output mode's sort with just this column, but keep any sort entries that
993
+ // belong to the other output modes so each mode keeps its own independent sort.
994
+
995
+ var preserved = _.filter(chain, function (entry) {
996
+ return !sortEntryAppliesTo(entry);
997
+ });
998
+ chain = preserved.concat([deepCopy(spec)]);
999
+
1000
+ // Give immediate visual feedback before the view redraws.
1001
+ jQuery('button.wcdv_icon_button' + sortIcon_orientationClass + '.wcdv_sort_icon').each(function (i, elt) {
1002
+ replaceSortIndicator(elt);
1003
+ });
1004
+
1005
+ jQuery('button.wcdv_icon_button.' + sortIcon_class).each(function (i, elt) {
1006
+ replaceSortIndicator(elt, dir);
1007
+ });
1008
+ }
1009
+
1010
+ sortSpec[orientation] = chain;
896
1011
  self.view.setSort(sortSpec, self.makeProgress('Sort'));
897
1012
  };
898
1013
 
@@ -936,9 +1051,17 @@ GridTable.prototype._addSortingToHeader = function (data, orientation, spec, con
936
1051
 
937
1052
  sortIcon_menu.addItem(trans('GRID.TABLE.SORT_MENU.ASCENDING', name), 'arrow-up-narrow-wide', function () {
938
1053
  setSort('asc');
1054
+ }, null, {
1055
+ iconName: 'plus',
1056
+ label: trans('GRID.TABLE.SORT_MENU.ADD_TO_SORT'),
1057
+ callback: function () { setSort('asc', null, true); }
939
1058
  });
940
1059
  sortIcon_menu.addItem(trans('GRID.TABLE.SORT_MENU.DESCENDING', name), 'arrow-down-wide-narrow', function () {
941
1060
  setSort('desc');
1061
+ }, null, {
1062
+ iconName: 'plus',
1063
+ label: trans('GRID.TABLE.SORT_MENU.ADD_TO_SORT'),
1064
+ callback: function () { setSort('desc', null, true); }
942
1065
  });
943
1066
  sortIcon_menu.addSeparator();
944
1067
  }
@@ -954,9 +1077,21 @@ GridTable.prototype._addSortingToHeader = function (data, orientation, spec, con
954
1077
  //var aggType = aggInfo.instance.getType();
955
1078
  sortIcon_menu.addItem(trans('GRID.TABLE.SORT_MENU.ASCENDING', aggInfo.instance.getFullName()), 'arrow-up-narrow-wide', (function (n) {
956
1079
  return function () { setSort('asc', n); };
1080
+ })(aggNum), null, (function (n) {
1081
+ return {
1082
+ iconName: 'plus',
1083
+ label: trans('GRID.TABLE.SORT_MENU.ADD_TO_SORT'),
1084
+ callback: function () { setSort('asc', n, true); }
1085
+ };
957
1086
  })(aggNum));
958
1087
  sortIcon_menu.addItem(trans('GRID.TABLE.SORT_MENU.DESCENDING', aggInfo.instance.getFullName()), 'arrow-down-wide-narrow', (function (n) {
959
1088
  return function () { setSort('desc', n); };
1089
+ })(aggNum), null, (function (n) {
1090
+ return {
1091
+ iconName: 'plus',
1092
+ label: trans('GRID.TABLE.SORT_MENU.ADD_TO_SORT'),
1093
+ callback: function () { setSort('desc', n, true); }
1094
+ };
960
1095
  })(aggNum));
961
1096
  sortIcon_menu.addSeparator();
962
1097
  });
@@ -986,31 +1121,52 @@ GridTable.prototype._addSortingToHeader = function (data, orientation, spec, con
986
1121
  var sortSpec_copy = deepCopy(self.view.getSort());
987
1122
  var spec_copy = deepCopy(spec);
988
1123
 
989
- if (sortSpec_copy[orientation]) {
990
- var currentDir = sortSpec_copy[orientation].dir;
1124
+ if (sortSpec_copy && sortSpec_copy[orientation]) {
1125
+
1126
+ // The view stores each orientation's sort as a chain (array) of specs. Normalize to an array
1127
+ // so we can light up every column that participates in the sort, each with its 1-based
1128
+ // priority. Crucially, for grid tables that redraw when the view is updated, this is the only
1129
+ // way you're ever going to see what the sort is.
1130
+
1131
+ var chain = _.isArray(sortSpec_copy[orientation])
1132
+ ? sortSpec_copy[orientation]
1133
+ : [sortSpec_copy[orientation]]
1134
+ ;
991
1135
 
992
- // Delete things that would be in the view's spec that aren't in the spec we were provided by
993
- // the caller (because they're independent of the user interface reflecting the sort). This way
994
- // we can just do an object-object comparison to see if what we just made corresponds to the
995
- // sort that is already set in the view. Crucially, for grid tables that redraw when the view
996
- // is updated, this is the only way you're ever going to see what the sort is.
1136
+ // Restrict the chain to the entries that apply to the current output mode (plain, grouped, or
1137
+ // pivotted), so the priority numbers reflect only this mode's sort and aren't inflated by
1138
+ // sorts configured in the other modes.
997
1139
 
998
- delete sortSpec_copy[orientation].dir;
1140
+ var applicable = _.filter(chain, sortEntryAppliesTo);
999
1141
 
1000
- // Note that `aggNum` is an important part of the spec when sorting group or pivot aggregates
1001
- // (i.e. total rows/columns) because they have their own row/column, and aren't thrown together
1002
- // like cell aggregates are.
1142
+ // `aggNum` is an important part of the spec when sorting group or pivot aggregates (i.e. total
1143
+ // rows/columns) because they have their own row/column, and aren't thrown together like cell
1144
+ // aggregates are. When it's not relevant, ignore it on both sides of the comparison.
1003
1145
 
1004
1146
  if (spec.aggType == null) {
1005
- delete sortSpec_copy[orientation].aggNum;
1006
1147
  delete spec_copy.aggNum;
1007
1148
  }
1008
1149
 
1009
- self.logDebug(self.makeLogTag() + ' orientation = %s ; spec = %O ; current = %O ; dir = %s',
1010
- self.toString(), orientation, spec_copy, sortSpec_copy[orientation], currentDir);
1150
+ for (var pi = 0; pi < applicable.length; pi++) {
1151
+
1152
+ // Compare each chain entry against the spec we were provided, ignoring direction (which is
1153
+ // independent of the user interface reflecting the sort).
1154
+
1155
+ var entry = deepCopy(applicable[pi]);
1156
+ var entryDir = entry.dir;
1157
+ delete entry.dir;
1158
+ if (spec.aggType == null) {
1159
+ delete entry.aggNum;
1160
+ }
1161
+
1162
+ if (_.isEqual(entry, spec_copy)) {
1011
1163
 
1012
- if (_.isEqual(sortSpec_copy[orientation], spec_copy)) {
1013
- replaceSortIndicator(sortIcon_btn, currentDir);
1164
+ // Only show a numbered priority badge when the sort spans more than one column.
1165
+
1166
+ var priority = applicable.length > 1 ? pi + 1 : null;
1167
+ replaceSortIndicator(sortIcon_btn, entryDir, priority);
1168
+ break;
1169
+ }
1014
1170
  }
1015
1171
  }
1016
1172
  };
@@ -1600,6 +1756,15 @@ GridTable.prototype.draw = function (root, opts, cont) {
1600
1756
  }
1601
1757
  }
1602
1758
 
1759
+ if (self.features.cellSelect && data.isPlain) {
1760
+ if (typeof self._addCellSelectHandler !== 'function') {
1761
+ self.logWarning(self.makeLogTag() + ' Requested feature "cellSelect" is not available: `_addCellSelectHandler` method does not exist');
1762
+ }
1763
+ else {
1764
+ self._addCellSelectHandler();
1765
+ }
1766
+ }
1767
+
1603
1768
  if (self.features.rowReorder) {
1604
1769
  self._addRowReorderHandler();
1605
1770
  }
@@ -2161,6 +2326,130 @@ GridTable.prototype.getCsv = function () {
2161
2326
  return self.csv.toString();
2162
2327
  };
2163
2328
 
2329
+ // #_getPlainVisibleColumns {{{2
2330
+
2331
+ GridTable.prototype._getPlainVisibleColumns = function () {
2332
+ var self = this;
2333
+
2334
+ return _.filter(determineColumns(self.colConfig, self.data, self.typeInfo), function (field) {
2335
+ var fcc = self.colConfig.get(field) || {};
2336
+ return !fcc.isHidden;
2337
+ });
2338
+ };
2339
+
2340
+ // #_getPlainCellDisplayValue {{{2
2341
+
2342
+ GridTable.prototype._getPlainCellDisplayValue = function (field, cell) {
2343
+ var self = this;
2344
+ var fcc = self.colConfig.get(field) || {};
2345
+ var value = format(fcc, self.typeInfo.get(field), cell);
2346
+
2347
+ if (value instanceof Element) {
2348
+ return jQuery(value).text();
2349
+ }
2350
+ if (value instanceof jQuery) {
2351
+ return value.text();
2352
+ }
2353
+ if (value == null) {
2354
+ return '';
2355
+ }
2356
+ if (fcc.allowHtml && self.typeInfo.get(field).type === 'string' && typeof value === 'string' && value.charAt(0) === '<') {
2357
+ return jQuery(value).text();
2358
+ }
2359
+
2360
+ return value;
2361
+ };
2362
+
2363
+ // #_getPlainCellCopyValue {{{2
2364
+
2365
+ GridTable.prototype._getPlainCellCopyValue = function (rowData, field) {
2366
+ var self = this;
2367
+ var fcc = self.colConfig.get(field) || {};
2368
+ var cell = rowData[field] || {};
2369
+ var value = cell.cachedRender;
2370
+
2371
+ if (value == null) {
2372
+ value = cell.value;
2373
+ }
2374
+ if (value == null) {
2375
+ value = cell.orig;
2376
+ }
2377
+
2378
+ if (value instanceof Element) {
2379
+ return jQuery(value).text();
2380
+ }
2381
+ if (value instanceof jQuery) {
2382
+ return value.text();
2383
+ }
2384
+ if (value == null) {
2385
+ return '';
2386
+ }
2387
+ if (fcc.allowHtml && self.typeInfo.get(field).type === 'string' && typeof value === 'string' && value.charAt(0) === '<') {
2388
+ return jQuery(value).text();
2389
+ }
2390
+
2391
+ return value;
2392
+ };
2393
+
2394
+ // #getSelectedDataAsTsv {{{2
2395
+
2396
+ GridTable.prototype.getSelectedDataAsTsv = function () {
2397
+ var self = this;
2398
+ var selectedRows = self.getSelection().rows;
2399
+ var fields = self._getPlainVisibleColumns();
2400
+ var tsv = new Csv({
2401
+ separator: '\t'
2402
+ });
2403
+
2404
+ if (selectedRows.length === 0 || fields.length === 0) {
2405
+ return '';
2406
+ }
2407
+
2408
+ tsv.addRow();
2409
+ _.each(fields, function (field) {
2410
+ var fcc = self.colConfig.get(field) || {};
2411
+ tsv.addCol(fcc.displayText || field);
2412
+ });
2413
+
2414
+ _.each(selectedRows, function (rowData) {
2415
+ tsv.addRow();
2416
+ _.each(fields, function (field) {
2417
+ tsv.addCol(self._getPlainCellDisplayValue(field, rowData[field]));
2418
+ });
2419
+ });
2420
+
2421
+ return tsv.toString();
2422
+ };
2423
+
2424
+ // #getSelectedCellsAsTsv {{{2
2425
+
2426
+ GridTable.prototype.getSelectedCellsAsTsv = function () {
2427
+ var self = this;
2428
+ var selection = self.getCellSelection();
2429
+ var tsv = new Csv({
2430
+ separator: '\t'
2431
+ });
2432
+
2433
+ if (selection.rowNums.length === 0 || selection.fields.length === 0) {
2434
+ return '';
2435
+ }
2436
+
2437
+ _.each(selection.rowNums, function (rowNum) {
2438
+ var rowData = self.data.dataByRowId[rowNum];
2439
+
2440
+ if (rowData == null) {
2441
+ return;
2442
+ }
2443
+
2444
+ tsv.addRow();
2445
+ _.each(selection.fields, function (field) {
2446
+ tsv.addCol(self._getPlainCellCopyValue(rowData, field));
2447
+ });
2448
+ });
2449
+
2450
+ return tsv.toString();
2451
+ };
2452
+
2164
2453
  // #getSelection {{{2
2165
2454
 
2166
2455
  /**
@@ -2191,6 +2480,63 @@ GridTable.prototype.getSelection = function () {
2191
2480
  };
2192
2481
  };
2193
2482
 
2483
+ // #getCellSelection {{{2
2484
+
2485
+ GridTable.prototype.getCellSelection = function () {
2486
+ var self = this;
2487
+ var selection = self.cellSelection;
2488
+ var rowNums = [];
2489
+ var fields = [];
2490
+ var cells = [];
2491
+
2492
+ if (selection == null || !_.isArray(selection.rowNums) || !_.isArray(selection.fields)) {
2493
+ return {
2494
+ rowNums: rowNums,
2495
+ fields: fields,
2496
+ cells: cells
2497
+ };
2498
+ }
2499
+
2500
+ rowNums = selection.rowNums.slice();
2501
+ fields = selection.fields.slice();
2502
+
2503
+ _.each(rowNums, function (rowNum) {
2504
+ _.each(fields, function (field) {
2505
+ cells.push({
2506
+ rowNum: rowNum,
2507
+ field: field
2508
+ });
2509
+ });
2510
+ });
2511
+
2512
+ return {
2513
+ rowNums: rowNums,
2514
+ fields: fields,
2515
+ cells: cells,
2516
+ anchor: selection.anchor,
2517
+ focus: selection.focus
2518
+ };
2519
+ };
2520
+
2521
+ // #clearCellSelection {{{2
2522
+
2523
+ GridTable.prototype.clearCellSelection = function () {
2524
+ var self = this;
2525
+ var prevSelection = self.getCellSelection();
2526
+
2527
+ if (prevSelection.cells.length === 0) {
2528
+ return;
2529
+ }
2530
+
2531
+ self.cellSelection = null;
2532
+
2533
+ if (typeof self._clearCellSelectionGui === 'function') {
2534
+ self._clearCellSelectionGui();
2535
+ }
2536
+
2537
+ self.fire('cellSelectionChange', null, self.getCellSelection());
2538
+ };
2539
+
2194
2540
  // #setSelection {{{2
2195
2541
 
2196
2542
  /**
@@ -2206,6 +2552,8 @@ GridTable.prototype.setSelection = function (what) {
2206
2552
  var self = this;
2207
2553
  var data = self.data.data;
2208
2554
 
2555
+ self.clearCellSelection();
2556
+
2209
2557
  if (!self.features.rowSelect) {
2210
2558
  return;
2211
2559
  }
@@ -2261,6 +2609,8 @@ GridTable.prototype.select = function (what) {
2261
2609
  var self = this;
2262
2610
  var data = self.data.data;
2263
2611
 
2612
+ self.clearCellSelection();
2613
+
2264
2614
  if (self.data.isGroup) {
2265
2615
  data = _.flatten(data);
2266
2616
  }
@@ -2321,6 +2671,8 @@ GridTable.prototype.unselect = function (what) {
2321
2671
  var self = this;
2322
2672
  var data = self.data.data;
2323
2673
 
2674
+ self.clearCellSelection();
2675
+
2324
2676
  if (self.data.isGroup) {
2325
2677
  data = _.flatten(data);
2326
2678
  }
@@ -42,9 +42,22 @@ var PopupMenu = makeSubclass('PopupMenu', Object, function () {
42
42
  *
43
43
  * @param {*} [userdata]
44
44
  * Arbitrary data passed to the callback when this item is clicked.
45
+ *
46
+ * @param {object} [action]
47
+ * An optional secondary action rendered as a trailing button on the right of the item. Clicking it
48
+ * does not trigger the item's primary callback.
49
+ *
50
+ * @param {string} action.iconName
51
+ * A Lucide icon name for the action button.
52
+ *
53
+ * @param {string} action.label
54
+ * Accessible label and tooltip for the action button.
55
+ *
56
+ * @param {function} action.callback
57
+ * Called when the action button is clicked. Receives `userdata` as its argument.
45
58
  */
46
59
 
47
- PopupMenu.prototype.addItem = function (label, iconName, callback, userdata) {
60
+ PopupMenu.prototype.addItem = function (label, iconName, callback, userdata, action) {
48
61
  var self = this;
49
62
 
50
63
  self.items.push({
@@ -52,6 +65,7 @@ PopupMenu.prototype.addItem = function (label, iconName, callback, userdata) {
52
65
  iconName: iconName,
53
66
  callback: callback,
54
67
  userdata: userdata,
68
+ action: action,
55
69
  separator: false
56
70
  });
57
71
  };
@@ -128,6 +142,34 @@ PopupMenu.prototype.open = function (anchorElement) {
128
142
  });
129
143
  })(entry);
130
144
 
145
+ // Optionally render a trailing action button that performs a secondary action without
146
+ // triggering the item's primary callback.
147
+ if (entry.action) {
148
+ var actionBtn = document.createElement('button');
149
+ actionBtn.className = 'wcdv-popup-menu-item-action wcdv_icon_button';
150
+ actionBtn.setAttribute('type', 'button');
151
+ if (entry.action.label) {
152
+ actionBtn.setAttribute('aria-label', entry.action.label);
153
+ actionBtn.setAttribute('title', entry.action.label);
154
+ }
155
+ if (entry.action.iconName) {
156
+ var actionIconElt = icon(entry.action.iconName).get(0);
157
+ if (actionIconElt) {
158
+ actionBtn.appendChild(actionIconElt);
159
+ }
160
+ }
161
+ (function (e) {
162
+ actionBtn.addEventListener('click', function (evt) {
163
+ evt.stopPropagation();
164
+ self.close();
165
+ if (typeof e.action.callback === 'function') {
166
+ e.action.callback(e.userdata);
167
+ }
168
+ });
169
+ })(entry);
170
+ item.appendChild(actionBtn);
171
+ }
172
+
131
173
  root.appendChild(item);
132
174
  }
133
175
 
@@ -0,0 +1,64 @@
1
+ import jQuery from 'jquery';
2
+
3
+ import {
4
+ makeSubclass,
5
+ } from '../util/misc.js';
6
+
7
+ var Toast = makeSubclass('Toast', Object, function () {
8
+ var self = this;
9
+
10
+ self._hideTimer = null;
11
+
12
+ self.ui = {
13
+ root: jQuery('<div>', {
14
+ 'class': 'wcdv_toast',
15
+ 'role': 'status',
16
+ 'aria-live': 'polite',
17
+ 'aria-atomic': 'true'
18
+ })
19
+ };
20
+
21
+ jQuery(document.body).append(self.ui.root);
22
+ });
23
+
24
+ Toast.prototype.show = function (text) {
25
+ var self = this;
26
+
27
+ if (self._hideTimer != null) {
28
+ clearTimeout(self._hideTimer);
29
+ self._hideTimer = null;
30
+ }
31
+
32
+ self.ui.root.text(text);
33
+ self.ui.root.addClass('wcdv_toast_visible');
34
+
35
+ self._hideTimer = setTimeout(function () {
36
+ self.hide();
37
+ }, 2000);
38
+ };
39
+
40
+ Toast.prototype.hide = function () {
41
+ var self = this;
42
+
43
+ if (self._hideTimer != null) {
44
+ clearTimeout(self._hideTimer);
45
+ self._hideTimer = null;
46
+ }
47
+
48
+ self.ui.root.removeClass('wcdv_toast_visible');
49
+ };
50
+
51
+ Toast.prototype.destroy = function () {
52
+ var self = this;
53
+
54
+ if (self._hideTimer != null) {
55
+ clearTimeout(self._hideTimer);
56
+ self._hideTimer = null;
57
+ }
58
+
59
+ if (self.ui != null && self.ui.root != null) {
60
+ self.ui.root.remove();
61
+ }
62
+ };
63
+
64
+ export default Toast;