-
Notifications
You must be signed in to change notification settings - Fork 19
/
facet_list.dart
299 lines (264 loc) · 8.64 KB
/
facet_list.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
import 'dart:async';
import 'package:algolia_insights/algolia_insights.dart';
import 'package:collection/collection.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:rxdart/rxdart.dart';
import 'disposable.dart';
import 'disposable_mixin.dart';
import 'extensions.dart';
import 'filter_state.dart';
import 'logger.dart';
import 'model/facet.dart';
import 'searcher/hits_searcher.dart';
import 'selectable_item.dart';
import 'sequencer.dart';
/// FacetList (refinement list) is a filtering components that displays facets,
/// and lets the user refine their search results by filtering on specific
/// values.
///
/// ## Create Facet List
///
/// Create [FacetList] with given [HitsSearcher] and [FilterState] components :
///
/// ```dart
/// // Create a HitsSearcher
/// final searcher = HitsSearcher(
/// applicationID: 'MY_APPLICATION_ID',
/// apiKey: 'MY_API_KEY',
/// indexName: 'MY_INDEX_NAME',
/// );
///
/// // Create a FilterState
/// final filterState = FilterState();
///
/// // Create a FacetList
/// final facetList = searcher.buildFacetList(
/// filterState: filterState,
/// attribute: 'MY_ATTRIBUTE',
/// );
/// ```
///
/// ## Get selectable facet lists
///
/// Get selectable facets changes by listening to [facets] submissions:
///
/// ```dart
/// facetList.facets.listen((facets) {
/// for (var facet in facets) {
/// print("${facet.item} ${facet.isSelected ? 'x' : '-'}");
/// }
/// });
/// ```
///
/// ### Toggle facet
///
/// Call [toggle] to selected/deselect a facet value:
///
/// ```dart
/// facetList.toggle('MY_FACET_VALUE');
/// ```
///
/// ## Dispose
///
/// Call [dispose] to release underlying resources:
///
/// ```dart
/// facetList.dispose();
/// ```
@experimental
abstract interface class FacetList implements Disposable {
/// Create [FacetList] instance.
factory FacetList({
required Stream<List<Facet>> facetsStream,
required SelectionState state,
SelectionMode selectionMode = SelectionMode.multiple,
bool persistent = false,
FilterEventTracker? eventTracker,
}) =>
_FacetList(
facetsStream: facetsStream,
state: state,
selectionMode: selectionMode,
persistent: persistent,
eventTracker: eventTracker,
);
/// Selection state
SelectionState get state;
/// Insights events tracking component
FilterEventTracker? get eventTracker;
/// Stream of [Facet] list with selection status.
Stream<List<SelectableFacet>> get facets;
/// Snapshot of the latest [facets] value.
List<SelectableFacet>? snapshot();
/// Select/deselect the provided facet value depending on the current
/// selection state.
void toggle(String value);
}
/// Elements selection mode.
enum SelectionMode { single, multiple }
/// [Facet] with selection status.
typedef SelectableFacet = SelectableItem<Facet>;
/// The `SelectionState` abstract class represents a way to manage a selection
/// state.
abstract interface class SelectionState {
/// Gets a stream of the current selection set.
///
/// This stream emits the latest set of selected items as `Set<String>`.
Stream<Set<String>> get selectionsStream;
/// Gets the current set of selected items.
///
/// This getter provides immediate access to the current selection state. It
/// returns a `Set<String>` representing the items that are currently
/// selected. This can be useful for obtaining the selection state at a
/// specific point in time.
Set<String> get selections;
/// Sets the current selection set.
///
/// This method allows for programmatically updating the set of selected
/// items.
void setSelections(
Set<String> selections,
);
}
/// Default implementation of [FacetList].
final class _FacetList with DisposableMixin implements FacetList {
/// Create [_FacetList] instance.
_FacetList({
required this.facetsStream,
required this.state,
required this.selectionMode,
required this.persistent,
required this.eventTracker,
}) {
_subscriptions
..add(_facets.connect())
..add(_inputFacets.connect())
..add(
_selectionsStream.connect(),
);
}
final Stream<List<Facet>> facetsStream;
@override
final FilterEventTracker? eventTracker;
/// Whether the facets can have single or multiple selections.
final SelectionMode selectionMode;
@override
final SelectionState state;
/// Should the selection be kept even if it does not match current results.
final bool persistent;
/// Events logger
final Logger _log = algoliaLogger('FacetList');
/// Selectable facets lists stream combining [_inputFacets]
/// and [_selectionsStream]
late final _facets = Rx.combineLatest2(
_inputFacets,
_selectionsStream,
(List<Facet> facets, Set<String> selections) => _selectableFacets(
facets,
selections,
persistent,
),
).distinct(const DeepCollectionEquality().equals).publishValue();
/// Stream of input facets lists values.
late final _inputFacets = facetsStream.publishValue();
/// Set of selected facet values from the filter state.
late final _selectionsStream = state.selectionsStream.publishValue();
/// Toggle operations sequencer.
final _sequencer = Sequencer();
/// Streams subscriptions composite.
final CompositeSubscription _subscriptions = CompositeSubscription();
@override
Stream<List<SelectableFacet>> get facets => _facets;
@override
List<SelectableFacet>? snapshot() => _facets.valueOrNull;
void _trackClickIfNeeded(String selection) {
_selectionsStream.first.then((selections) {
if (!selections.contains(selection)) {
eventTracker?.clickedFilters(
eventName: 'Filter Applied',
values: [selection],
);
}
});
}
@override
void toggle(String value) {
_sequencer.addOperation(() => _performToggle(value));
}
/// Perform toggle operation.
Future<void> _performToggle(String value) async {
_trackClickIfNeeded(value);
final currentSelections = state.selections;
_log.finest('current selections: $currentSelections -> $value selected');
final Set<String> selectionsToApply;
switch (selectionMode) {
case SelectionMode.single:
selectionsToApply = currentSelections.contains(value) ? {} : {value};
case SelectionMode.multiple:
final set = currentSelections.modifiable();
selectionsToApply = currentSelections.contains(value)
? (set..remove(value))
: (set..add(value));
}
state.setSelections(selectionsToApply);
}
/// Creates a list of `SelectableItem<Facet>` from a given list of `Facet`
/// and a set of selections.
///
/// This function maps each `Facet` in the provided list to a
/// `SelectableFacet` by checking if the facet's value is contained within the
/// given set of selections. Each `SelectableFacet` will have its `isSelected`
/// property set accordingly. For persistent selections, facets that are
/// currently selected but not present in the provided facet list will also be
/// included.
///
/// [facets]: The list of `Facet` from which the `SelectableFacet` list will
/// be created.
/// [selections]: The set of currently selected facet values. Each value in
/// this set corresponds to a `Facet`'s value that should be marked as
/// selected.
/// [persistent]: A boolean indicating whether selections should persist even
/// when they are not present in the current facet list. If true, facets that
/// are selected but not present in the provided list will be added to the
/// result with a count of 0.
///
/// Returns a list of `SelectableFacet`, which includes all facets from the
/// provided list marked as selected or not based on the selections set, and,
/// if persistent is true, any additional facets that are selected but not
/// present in the initial list.
static List<SelectableItem<Facet>> _selectableFacets(
List<Facet> facets,
Set<String> selections,
bool persistent,
) {
final facetList = facets
.map(
(facet) => SelectableFacet(
item: facet,
isSelected: selections.contains(facet.value),
),
)
.toList();
if (!persistent) {
return facetList;
}
final presentValues = facets.map((facet) => facet.value).toSet();
final persistentFacetList = selections
.whereNot(presentValues.contains)
.map(
(selection) => SelectableFacet(
item: Facet(selection, 0),
isSelected: true,
),
)
.toList();
return [...persistentFacetList, ...facetList];
}
@override
void doDispose() {
_log.finest('FacetList disposed');
_sequencer.dispose();
_subscriptions.dispose();
}
}