@@ -14,6 +14,10 @@ import {
1414 DataModel
1515} from '@lumino/datagrid' ;
1616
17+ import {
18+ Signal , ISignal
19+ } from '@lumino/signaling' ;
20+
1721import {
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
0 commit comments