Skip to content

Commit 001ca04

Browse files
authored
Adding a "Select all" checkbox to the "Filter by Value" menu (#146)
* fix dialog datamodel filter logic * add semicolons * add support for default checkboxes * add select_all box with basic event listener * add support for select all/none checkbox * fix an edge case where unselecting and reselecting all values does not render a ticked "select all" * fix alignment and styling * refactor get checked state logic tetsting * refactor check-all promise handling * fix hitting on "void" area, fix boolean datafilter issue, fix filter column index mismatch issue * semicolons * add support for default checkboxes * add select_all box with basic event listener * add support for select all/none checkbox * fix an edge case where unselecting and reselecting all values does not render a ticked "select all" * fix alignment and styling * refactor get checked state logic tetsting * refactor check-all promise handling * fix hitting on "void" area, fix boolean datafilter issue, fix filter column index mismatch issue * semicolons * change methods to private where applicable
1 parent b5723a2 commit 001ca04

File tree

5 files changed

+265
-11
lines changed

5 files changed

+265
-11
lines changed

src/core/filterMenu.ts

Lines changed: 247 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import {
1414
DataModel
1515
} from '@lumino/datagrid';
1616

17+
import {
18+
Signal, ISignal
19+
} from '@lumino/signaling';
20+
1721
import {
1822
ElementExt
1923
} from '@lumino/domutils';
@@ -91,13 +95,35 @@ export class InteractiveFilterDialog extends BoxPanel {
9195
this._applyWidget = new Widget();
9296
this._applyWidget.addClass('ipydatagrid-filter-apply')
9397

98+
// Create the "Select All" widget and connecting to
99+
// lumino signal
100+
this._selectAllCheckbox = new SelectCanvasWidget();
101+
this._connectToCheckbox();
102+
94103
// Add all widgets to the dock
95104
this.addWidget(this._titleWidget);
105+
this.addWidget(this._selectAllCheckbox);
96106
this.addWidget(this._filterByConditionWidget);
97107
this.addWidget(this._uniqueValueGrid);
98108
this.addWidget(this._applyWidget);
99109
}
100110

111+
/**
112+
* Connects to the "Select All" widget signal and
113+
* toggles checking all/none of the unique elements
114+
* by adding/removing them from the state object
115+
*/
116+
private _connectToCheckbox() {
117+
this._selectAllCheckbox.checkChanged.connect((sender: SelectCanvasWidget, checked: boolean) => {
118+
this.userInteractedWithDialog = true;
119+
120+
// Adding all unique values to the state **IF** the select
121+
// all box is "checked"
122+
this.addRemoveAllUniqueValuesToState(checked);
123+
})
124+
}
125+
126+
101127
/**
102128
* Checks for any undefined values in `this._filterValue`.
103129
*
@@ -131,6 +157,11 @@ export class InteractiveFilterDialog extends BoxPanel {
131157
return;
132158
}
133159

160+
if (!this.hasFilter && !this.userInteractedWithDialog) {
161+
this.close();
162+
return;
163+
}
164+
134165
const value = this._mode === 'condition'
135166
? <Transform.FilterValue>this._filterValue
136167
: this._uniqueValueStateManager.getValues(this.region, this._columnIndex);
@@ -180,8 +211,9 @@ export class InteractiveFilterDialog extends BoxPanel {
180211
private _render(): void {
181212
if (this._mode === 'condition') {
182213
this._applyWidget.node.style.minHeight = '65px';
183-
this._uniqueValueGrid.setHidden(true)
184-
this._filterByConditionWidget.setHidden(false)
214+
this._selectAllCheckbox.setHidden(true);
215+
this._uniqueValueGrid.setHidden(true);
216+
this._filterByConditionWidget.setHidden(false);
185217

186218
// selector
187219
VirtualDOM.render([
@@ -202,6 +234,7 @@ export class InteractiveFilterDialog extends BoxPanel {
202234

203235
} else if (this._mode === 'value') {
204236
this._applyWidget.node.style.minHeight = '30px';
237+
this._selectAllCheckbox.setHidden(false);
205238
this._uniqueValueGrid.setHidden(false);
206239
this._filterByConditionWidget.setHidden(true);
207240

@@ -251,6 +284,38 @@ export class InteractiveFilterDialog extends BoxPanel {
251284
});
252285
}
253286

287+
/**
288+
* Checks whether all unique elements in the column
289+
* are present as "selected" in the state. This
290+
* function is used to determine whether the
291+
* "Select all" button should be ticked when
292+
* opening the filter by value menu.
293+
*/
294+
updateSelectAllCheckboxState() {
295+
if (!this.userInteractedWithDialog && !this.hasFilter) {
296+
this._selectAllCheckbox.checked = true;
297+
return;
298+
}
299+
300+
const uniqueVals = this._model.uniqueValues(
301+
this._region,
302+
this._columnIndex
303+
);
304+
305+
uniqueVals.then(values => {
306+
let showAsChecked = true;
307+
for (const value of values) {
308+
// If there is a unique value which is not present in the state then it is
309+
// not ticked, and therefore we should not tick the "Select all" checkbox.
310+
if (!this._uniqueValueStateManager.has(this._region, this._columnIndex, value)) {
311+
showAsChecked = false;
312+
break;
313+
}
314+
}
315+
this._selectAllCheckbox.checked = showAsChecked;
316+
});
317+
}
318+
254319
/**
255320
* Open the menu at the specified location.
256321
*
@@ -268,6 +333,14 @@ export class InteractiveFilterDialog extends BoxPanel {
268333
this._region = options.region;
269334
this._mode = options.mode;
270335

336+
// Setting filter flag
337+
this.hasFilter = this._model.getFilterTransform(this.model.getSchemaIndex(this._region, this._columnIndex)) !== undefined;
338+
339+
this.userInteractedWithDialog = false;
340+
341+
// Determines whether we should or not tick the "Select all" chekcbox
342+
this.updateSelectAllCheckboxState();
343+
271344
// Update styling on unique value grid
272345
this._uniqueValueGrid.style = {
273346
voidColor: Theme.getBackgroundColor(),
@@ -841,6 +914,23 @@ export class InteractiveFilterDialog extends BoxPanel {
841914
]
842915
}
843916

917+
async addRemoveAllUniqueValuesToState(add: boolean) {
918+
const uniqueVals = this.model.uniqueValues(
919+
this._region,
920+
this._columnIndex
921+
);
922+
923+
return uniqueVals.then(values => {
924+
for (let value of values) {
925+
if (add) {
926+
this._uniqueValueStateManager.add(this._region, this._columnIndex, value);
927+
} else {
928+
this._uniqueValueStateManager.remove(this._region, this._columnIndex, value);
929+
}
930+
}
931+
});
932+
}
933+
844934
/**
845935
* Returns a reference to the data model used for this menu.
846936
*/
@@ -910,6 +1000,138 @@ export class InteractiveFilterDialog extends BoxPanel {
9101000

9111001
// Unique value state
9121002
private _uniqueValueStateManager: UniqueValueStateManager
1003+
1004+
// Checking filter status
1005+
hasFilter: boolean = false;
1006+
userInteractedWithDialog: boolean = false;
1007+
1008+
private _selectAllCheckbox: SelectCanvasWidget;
1009+
}
1010+
1011+
/**
1012+
* A lumino widget to draw and control the
1013+
* "Select All" checkbox
1014+
*/
1015+
class SelectCanvasWidget extends Widget {
1016+
constructor() {
1017+
super();
1018+
this.canvas = document.createElement("canvas");
1019+
this.node.style.minHeight = "16px";
1020+
this.node.style.overflow = "visible";
1021+
this.node.appendChild(this.canvas);
1022+
}
1023+
1024+
get checked(): boolean {
1025+
return this._checked;
1026+
}
1027+
1028+
/**
1029+
* We re-render reach time the box is checked
1030+
*/
1031+
set checked(value: boolean) {
1032+
this._checked = value;
1033+
this.renderCheckbox();
1034+
}
1035+
1036+
get checkChanged(): ISignal<this, boolean> {
1037+
return this._checkedChanged;
1038+
}
1039+
1040+
/**
1041+
* Toggles and checkbox value and emits
1042+
* a signal to add all unique values to
1043+
* the state
1044+
*/
1045+
toggleCheckMark = () => {
1046+
this._checked = !this._checked;
1047+
this.renderCheckbox();
1048+
this._checkedChanged.emit(this._checked);
1049+
}
1050+
1051+
/**
1052+
* Rendering the actual tickmark inside the
1053+
* canvas box. This function is only called
1054+
* from within renderCheckbox() below
1055+
*/
1056+
addCheckMark() {
1057+
const gc = this.canvas.getContext('2d')!;
1058+
const BOX_OFFSET = 8;
1059+
const x = 0;
1060+
const y = 0;
1061+
gc.lineWidth = 1;
1062+
gc.beginPath();
1063+
gc.strokeStyle = "#000000";
1064+
gc.moveTo(x + BOX_OFFSET + 3, y + BOX_OFFSET + 5);
1065+
gc.lineTo(x + BOX_OFFSET + 4, y + BOX_OFFSET + 8);
1066+
gc.lineTo(x + BOX_OFFSET + 8, y + BOX_OFFSET + 2);
1067+
gc.lineWidth = 2;
1068+
gc.stroke();
1069+
}
1070+
1071+
1072+
/**
1073+
* Renders the checkbox and tick mark. Tick mark
1074+
* rendering is conditional
1075+
*/
1076+
renderCheckbox() {
1077+
const gc = this.canvas.getContext('2d')!;
1078+
1079+
// Needed to avoid blurring issue.
1080+
// Set display size (css pixels).
1081+
const size = 100;
1082+
this.canvas.style.width = size + "px";
1083+
this.canvas.style.height = size + "px";
1084+
1085+
// Set actual size in memory (scaled to account for extra pixel density)
1086+
var scale = window.devicePixelRatio;
1087+
this.canvas.width = Math.floor(size * scale);
1088+
this.canvas.height = Math.floor(size * scale);
1089+
1090+
// Normalize coordinate system to use css pixels.
1091+
gc.scale(scale, scale);
1092+
1093+
// Draw the checkmark rectangle
1094+
const BOX_OFFSET = 8;
1095+
const x = 0;
1096+
const y = 0;
1097+
gc.lineWidth = 1;
1098+
gc.fillStyle = '#ffffff'
1099+
gc.fillRect(x + BOX_OFFSET, y + BOX_OFFSET, 10, 10)
1100+
gc.strokeStyle = 'black';
1101+
gc.strokeRect(x + BOX_OFFSET, y + BOX_OFFSET, 10, 10)
1102+
1103+
// Draw "Select all" text
1104+
gc.font = "12px sans-serif";
1105+
gc.fillStyle = Theme.getFontColor(0);
1106+
gc.fillText("(Select All)", x + 30, y + 17);
1107+
1108+
// Draw actual tickmark inside the checkmark rect
1109+
if (this._checked) {
1110+
this.addCheckMark();
1111+
}
1112+
}
1113+
1114+
/**
1115+
* Adding an event listener for clicks in the box
1116+
* area and rendering the checkbox
1117+
*/
1118+
onAfterAttach() {
1119+
this.renderCheckbox();
1120+
this.canvas.addEventListener('click', this.toggleCheckMark, true);
1121+
}
1122+
1123+
/**
1124+
* Removing the event listener to declutter the
1125+
* DOM space
1126+
* @param msg lumino msg
1127+
*/
1128+
protected onAfterDetach(msg: Message): void {
1129+
this.canvas.removeEventListener('click', this.toggleCheckMark, true);
1130+
}
1131+
1132+
private canvas: HTMLCanvasElement;
1133+
private _checked: boolean = false;
1134+
private _checkedChanged = new Signal<this, boolean>(this);
9131135
}
9141136

9151137
/**
@@ -1046,15 +1268,35 @@ class UniqueValueGridMouseHandler extends BasicMouseHandler {
10461268
//@ts-ignore added so we don't have to add basicmousehandler.ts fork
10471269
onMouseDown(grid: DataGrid, event: MouseEvent): void {
10481270
const hit = grid.hitTest(event.clientX, event.clientY);
1271+
1272+
// Bail if hitting on an invalid area
1273+
if (hit.region === "void") {
1274+
return;
1275+
}
10491276
const row = hit.row;
10501277
const colIndex = this._filterDialog.columnIndex;
10511278
const region = this._filterDialog.region;
10521279
const value = grid.dataModel!.data('body', row, 0);
10531280

1054-
if (this._uniqueValuesSelectionState.has(region, colIndex, value)) {
1055-
this._uniqueValuesSelectionState.remove(region, colIndex, value)
1281+
const updateCheckState = () => {
1282+
if (this._uniqueValuesSelectionState.has(region, colIndex, value)) {
1283+
this._uniqueValuesSelectionState.remove(region, colIndex, value);
1284+
} else {
1285+
this._uniqueValuesSelectionState.add(region, colIndex, value);
1286+
}
1287+
1288+
// Updating the "Select all" chexboox if needed
1289+
this._filterDialog.updateSelectAllCheckboxState();
1290+
}
1291+
1292+
// User is clicking for the first time when no filter is applied
1293+
if (!this._filterDialog.hasFilter && !this._filterDialog.userInteractedWithDialog) {
1294+
this._filterDialog.addRemoveAllUniqueValuesToState(true).then(() => {
1295+
this._filterDialog.userInteractedWithDialog = true;
1296+
updateCheckState();
1297+
});
10561298
} else {
1057-
this._uniqueValuesSelectionState.add(region, colIndex, value)
1299+
updateCheckState();
10581300
}
10591301
}
10601302

src/core/transformExecutors.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,6 @@ class FilterExecutor extends TransformExecutor {
134134
case "in":
135135
filterFunc = (item: any) => {
136136
let values = <any[]>this._options.value;
137-
138-
if (this._options.dType === 'boolean'){
139-
return values.includes(String(item[this._options.field]))
140-
}
141137
return values.includes(item[this._options.field]);
142138
};
143139
break;

src/core/transformStateManager.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,14 @@ export class TransformStateManager {
230230
return transforms;
231231
}
232232

233+
getFilterTransform(columnIndex: number): Transform.TransformSpec | undefined {
234+
if (!this._state.hasOwnProperty(columnIndex)) {
235+
return undefined;
236+
}
237+
238+
return this._state[columnIndex].filter;
239+
}
240+
233241
private _state: TransformStateManager.IState = {};
234242
private _changed = new Signal<this, TransformStateManager.IEvent>(this);
235243
}

src/core/valueRenderer.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,7 @@ export class FilterValueRenderer extends TextRenderer {
131131
gc.strokeRect(config.x + BOX_OFFSET, config.y + BOX_OFFSET, 10, 10)
132132

133133
// Check state to display checkbox
134-
135-
if (this._stateManager.has(this._dialog.region, this._dialog.columnIndex, config.value)) {
134+
if (this._getCheckedState(config)) {
136135
gc.beginPath();
137136
gc.strokeStyle = "#000000";
138137
gc.moveTo(config.x + BOX_OFFSET + 3, config.y + BOX_OFFSET + 5);
@@ -143,6 +142,11 @@ export class FilterValueRenderer extends TextRenderer {
143142
}
144143
}
145144

145+
private _getCheckedState(config: CellRenderer.CellConfig): boolean {
146+
return (this._stateManager.has(this._dialog.region, this._dialog.columnIndex, config.value)
147+
|| (!this._dialog.hasFilter && !this._dialog.userInteractedWithDialog))
148+
}
149+
146150
private _stateManager: UniqueValueStateManager
147151
private _dialog: InteractiveFilterDialog
148152
}

src/core/viewbasedjsonmodel.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,10 @@ export class ViewBasedJSONModel extends MutableDataModel {
373373
return this._transformState.activeTransforms;
374374
}
375375

376+
getFilterTransform(columnIndex: number): Transform.TransformSpec | undefined {
377+
return this._transformState.getFilterTransform(columnIndex);
378+
}
379+
376380
/**
377381
* Updates the indicated value in the dataset.
378382
*

0 commit comments

Comments
 (0)