@quenty/observablecollection 12.11.1 → 12.12.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.
- package/CHANGELOG.md +27 -0
- package/package.json +10 -9
- package/src/Shared/ObservableCountingMap.lua +9 -0
- package/src/Shared/ObservableList.lua +37 -28
- package/src/Shared/ObservableMap.lua +10 -0
- package/src/Shared/ObservableSet.lua +9 -0
- package/src/Shared/SortedList/ObservableSortedList.lua +586 -0
- package/src/Shared/{ObservableSortedList.story.lua → SortedList/ObservableSortedList.story.lua} +15 -12
- package/src/Shared/{ObservableSortedList.lua → SortedList/ObservableSortedListOld.lua} +37 -74
- package/src/Shared/SortedList/ObservableSortedList_Performance.story.lua +74 -0
- package/src/Shared/SortedList/ObservableSortedList_Print.story.lua +65 -0
- package/src/Shared/SortedList/SortFunctionUtils.lua +31 -0
- package/src/Shared/SortedList/SortedNode.lua +1171 -0
- package/src/Shared/SortedList/SortedNodeValue.lua +53 -0
- package/src/Shared/Utils/ListIndexUtils.lua +39 -0
- package/test/default.project.json +1 -7
- /package/src/Shared/{ObservableSortedList.spec.lua → SortedList/ObservableSortedList.spec.lua} +0 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
A list that can be observed for blend and other components and maintains sorting order.
|
|
3
|
+
|
|
4
|
+
This allows you to observe both an index, observe a value at an index, and more.
|
|
5
|
+
|
|
6
|
+
This class is a red-black binary sorted tree. Unlike previous iterations of this class, we can add
|
|
7
|
+
values in log(n) time, and remove in log(n) time, and it uses less memory.
|
|
8
|
+
|
|
9
|
+
Previously we'd use O(n^2) processing time when constructing this class.
|
|
10
|
+
|
|
11
|
+
We reuse the node itself as the indexing key.
|
|
12
|
+
|
|
13
|
+
This class always prefers to add equivalent elements to the end of the list if they're not in the list.
|
|
14
|
+
Otherwise it prefers minimal movement.
|
|
15
|
+
|
|
16
|
+
@class ObservableSortedList
|
|
17
|
+
]=]
|
|
18
|
+
|
|
19
|
+
local require = require(script.Parent.loader).load(script)
|
|
20
|
+
|
|
21
|
+
local Brio = require("Brio")
|
|
22
|
+
local DuckTypeUtils = require("DuckTypeUtils")
|
|
23
|
+
local Maid = require("Maid")
|
|
24
|
+
local Observable = require("Observable")
|
|
25
|
+
local ObservableSubscriptionTable = require("ObservableSubscriptionTable")
|
|
26
|
+
local Rx = require("Rx")
|
|
27
|
+
local Signal = require("Signal")
|
|
28
|
+
local SortedNode = require("SortedNode")
|
|
29
|
+
local SortedNodeValue = require("SortedNodeValue")
|
|
30
|
+
local ValueObject = require("ValueObject")
|
|
31
|
+
local SortFunctionUtils = require("SortFunctionUtils")
|
|
32
|
+
local ListIndexUtils = require("ListIndexUtils")
|
|
33
|
+
|
|
34
|
+
local ObservableSortedList = {}
|
|
35
|
+
ObservableSortedList.ClassName = "ObservableSortedList"
|
|
36
|
+
ObservableSortedList.__index = ObservableSortedList
|
|
37
|
+
|
|
38
|
+
--[=[
|
|
39
|
+
Constructs a new ObservableSortedList
|
|
40
|
+
@param isReversed boolean
|
|
41
|
+
@param compare function
|
|
42
|
+
@return ObservableSortedList<T>
|
|
43
|
+
]=]
|
|
44
|
+
function ObservableSortedList.new(isReversed, compare)
|
|
45
|
+
assert(type(isReversed) == "boolean" or isReversed == nil, "Bad isReversed")
|
|
46
|
+
|
|
47
|
+
local self = setmetatable({}, ObservableSortedList)
|
|
48
|
+
|
|
49
|
+
self._maid = Maid.new()
|
|
50
|
+
|
|
51
|
+
self._indexObservers = self._maid:Add(ObservableSubscriptionTable.new())
|
|
52
|
+
self._nodeIndexObservables = self._maid:Add(ObservableSubscriptionTable.new())
|
|
53
|
+
|
|
54
|
+
self._mainObservables = self._maid:Add(ObservableSubscriptionTable.new())
|
|
55
|
+
|
|
56
|
+
self._nodesAdded = {}
|
|
57
|
+
self._nodesRemoved = {}
|
|
58
|
+
self._lowestIndexChanged = nil
|
|
59
|
+
|
|
60
|
+
self._compare = if isReversed then SortFunctionUtils.reverse(compare) else compare
|
|
61
|
+
|
|
62
|
+
self._countValue = self._maid:Add(ValueObject.new(0, "number"))
|
|
63
|
+
|
|
64
|
+
--[=[
|
|
65
|
+
Fires when an item is added
|
|
66
|
+
|
|
67
|
+
@readonly
|
|
68
|
+
@prop ItemAdded Signal<T, number, Symbol>
|
|
69
|
+
@within ObservableSortedList
|
|
70
|
+
]=]
|
|
71
|
+
self.ItemAdded = self._maid:Add(Signal.new())
|
|
72
|
+
|
|
73
|
+
--[=[
|
|
74
|
+
Fires when an item is removed.
|
|
75
|
+
|
|
76
|
+
@readonly
|
|
77
|
+
@prop ItemRemoved Signal<T, Symbol>
|
|
78
|
+
@within ObservableSortedList
|
|
79
|
+
]=]
|
|
80
|
+
self.ItemRemoved = Signal.new()
|
|
81
|
+
|
|
82
|
+
--[=[
|
|
83
|
+
Fires when the order could have changed
|
|
84
|
+
|
|
85
|
+
@readonly
|
|
86
|
+
@prop OrderChanged Signal
|
|
87
|
+
@within ObservableSortedList
|
|
88
|
+
]=]
|
|
89
|
+
self.OrderChanged = self._maid:Add(Signal.new())
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
--[=[
|
|
93
|
+
Fires when the count changes
|
|
94
|
+
|
|
95
|
+
@readonly
|
|
96
|
+
@prop CountChanged Signal<number>
|
|
97
|
+
@within ObservableSortedList
|
|
98
|
+
]=]
|
|
99
|
+
self.CountChanged = self._countValue.Changed
|
|
100
|
+
|
|
101
|
+
return self
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
--[=[
|
|
105
|
+
Returns whether the value is an observable list
|
|
106
|
+
@param value any
|
|
107
|
+
@return boolean
|
|
108
|
+
]=]
|
|
109
|
+
function ObservableSortedList.isObservableSortedList(value)
|
|
110
|
+
return DuckTypeUtils.isImplementation(ObservableSortedList, value)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
--[=[
|
|
114
|
+
Observes the list, allocating a new list in the process.
|
|
115
|
+
|
|
116
|
+
@return Observable<{ T }>
|
|
117
|
+
]=]
|
|
118
|
+
function ObservableSortedList:Observe()
|
|
119
|
+
return self._mainObservables:Observe("list")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
--[=[
|
|
123
|
+
Allows iteration over the observable map
|
|
124
|
+
|
|
125
|
+
@return (T) -> ((T, nextIndex: any) -> ...any, T?)
|
|
126
|
+
]=]
|
|
127
|
+
function ObservableSortedList:__iter()
|
|
128
|
+
if self._root then
|
|
129
|
+
return self._root:IterateData()
|
|
130
|
+
else
|
|
131
|
+
return SortFunctionUtils.emptyIterator
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
--[=[
|
|
136
|
+
Iterates over an index range
|
|
137
|
+
|
|
138
|
+
@param start number
|
|
139
|
+
@param finish number
|
|
140
|
+
@return (T) -> ((T, nextIndex: any) -> ...any, T?)
|
|
141
|
+
]=]
|
|
142
|
+
function ObservableSortedList:IterateRange(start, finish)
|
|
143
|
+
return coroutine.wrap(function()
|
|
144
|
+
for index, node in self:_iterateNodesRange(start, finish) do
|
|
145
|
+
coroutine.yield(index, node.data)
|
|
146
|
+
end
|
|
147
|
+
end)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
function ObservableSortedList:_iterateNodes()
|
|
151
|
+
if self._root then
|
|
152
|
+
return self._root:IterateNodes()
|
|
153
|
+
else
|
|
154
|
+
return SortFunctionUtils.emptyIterator
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
function ObservableSortedList:_iterateNodesRange(start, finish)
|
|
159
|
+
if self._root then
|
|
160
|
+
return self._root:IterateNodesRange(start, finish)
|
|
161
|
+
else
|
|
162
|
+
return SortFunctionUtils.emptyIterator
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
function ObservableSortedList:_containsNode(node)
|
|
167
|
+
assert(SortedNode.isSortedNode(node), "Bad node")
|
|
168
|
+
|
|
169
|
+
if self._root then
|
|
170
|
+
return self._root:ContainsNode(node)
|
|
171
|
+
else
|
|
172
|
+
return false
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
function ObservableSortedList:_findNodeForDataLinearSearchSlow(data)
|
|
177
|
+
if self._root then
|
|
178
|
+
return self._root:FindFirstNodeForData(data)
|
|
179
|
+
else
|
|
180
|
+
return nil
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
function ObservableSortedList:_findNodeAtIndex(index)
|
|
185
|
+
assert(type(index) == "number", "Bad index")
|
|
186
|
+
|
|
187
|
+
if self._root then
|
|
188
|
+
return self._root:FindNodeAtIndex(index)
|
|
189
|
+
else
|
|
190
|
+
return nil
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
function ObservableSortedList:_findNodeIndex(node)
|
|
195
|
+
assert(SortedNode.isSortedNode(node), "Bad node")
|
|
196
|
+
|
|
197
|
+
if self._root then
|
|
198
|
+
return self._root:FindNodeIndex(node)
|
|
199
|
+
else
|
|
200
|
+
return nil
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
--[=[
|
|
205
|
+
Gets the first node for a given symbol
|
|
206
|
+
|
|
207
|
+
@param content T
|
|
208
|
+
@return Symbol
|
|
209
|
+
]=]
|
|
210
|
+
function ObservableSortedList:FindFirstKey(content)
|
|
211
|
+
return self:_findNodeForDataLinearSearchSlow(content)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
function ObservableSortedList:PrintDebug()
|
|
215
|
+
print(self._root)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
--[=[
|
|
219
|
+
Returns true if the value exists
|
|
220
|
+
|
|
221
|
+
@param content T
|
|
222
|
+
@return boolean
|
|
223
|
+
]=]
|
|
224
|
+
function ObservableSortedList:Contains(content)
|
|
225
|
+
assert(content ~= nil, "Bad content")
|
|
226
|
+
|
|
227
|
+
-- TODO: Speed up
|
|
228
|
+
return self:_findNodeForDataLinearSearchSlow(content) ~= nil
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
--[=[
|
|
232
|
+
Observes all items in the list
|
|
233
|
+
@return Observable<Brio<T, Symbol>>
|
|
234
|
+
]=]
|
|
235
|
+
function ObservableSortedList:ObserveItemsBrio()
|
|
236
|
+
return Observable.new(function(sub)
|
|
237
|
+
local maid = Maid.new()
|
|
238
|
+
|
|
239
|
+
-- TODO: Optimize this so we don't have to make so many brios and connect
|
|
240
|
+
-- to so many events
|
|
241
|
+
|
|
242
|
+
local function handleItem(data, _index, node)
|
|
243
|
+
local brio = Brio.new(data, node)
|
|
244
|
+
maid[node] = brio
|
|
245
|
+
sub:Fire(brio)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
-- NOTE: This can modify the list...?
|
|
249
|
+
for index, node in self:_iterateNodes() do
|
|
250
|
+
handleItem(node.data, index, node)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
maid:GiveTask(self.ItemAdded:Connect(handleItem))
|
|
254
|
+
maid:GiveTask(self.ItemRemoved:Connect(function(_item, node)
|
|
255
|
+
maid[node] = nil
|
|
256
|
+
end))
|
|
257
|
+
|
|
258
|
+
-- TODO: Prevent this stuff from happening too
|
|
259
|
+
self._maid[sub] = maid
|
|
260
|
+
maid:GiveTask(function()
|
|
261
|
+
self._maid[sub] = nil
|
|
262
|
+
sub:Complete()
|
|
263
|
+
end)
|
|
264
|
+
|
|
265
|
+
return maid
|
|
266
|
+
end)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
--[=[
|
|
270
|
+
Observes the index as it changes, until the entry at the existing
|
|
271
|
+
index is removed.
|
|
272
|
+
|
|
273
|
+
@param indexToObserve number
|
|
274
|
+
@return Observable<number>
|
|
275
|
+
]=]
|
|
276
|
+
function ObservableSortedList:ObserveIndex(indexToObserve)
|
|
277
|
+
assert(type(indexToObserve) == "number", "Bad indexToObserve")
|
|
278
|
+
|
|
279
|
+
local node = self:_findNodeAtIndex(indexToObserve)
|
|
280
|
+
if not node then
|
|
281
|
+
error(string.format("[ObservableSortedList.ObserveIndex] - No entry at index %q, cannot observe changes", indexToObserve))
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
return self:ObserveIndexByKey(node)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
--[=[
|
|
288
|
+
Observes the current value at a given index. This can be useful for observing
|
|
289
|
+
the first entry, or matching stuff up to a given slot.
|
|
290
|
+
|
|
291
|
+
@param indexToObserve number
|
|
292
|
+
@return Observable<(T, Key)>
|
|
293
|
+
]=]
|
|
294
|
+
function ObservableSortedList:ObserveAtIndex(indexToObserve)
|
|
295
|
+
assert(type(indexToObserve) == "number", "Bad indexToObserve")
|
|
296
|
+
|
|
297
|
+
return self._indexObservers:Observe(indexToObserve)
|
|
298
|
+
:Pipe({
|
|
299
|
+
Rx.start(function()
|
|
300
|
+
local node = self:_findNodeAtIndex(indexToObserve)
|
|
301
|
+
if node then
|
|
302
|
+
return node.data, node
|
|
303
|
+
else
|
|
304
|
+
return nil
|
|
305
|
+
end
|
|
306
|
+
end);
|
|
307
|
+
})
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
--[=[
|
|
311
|
+
Observes the index as it changes, until the entry at the existing
|
|
312
|
+
node is removed.
|
|
313
|
+
|
|
314
|
+
@param node SortedNode
|
|
315
|
+
@return Observable<number>
|
|
316
|
+
]=]
|
|
317
|
+
function ObservableSortedList:ObserveIndexByKey(node)
|
|
318
|
+
assert(SortedNode.isSortedNode(node), "Bad node")
|
|
319
|
+
|
|
320
|
+
return self._nodeIndexObservables:Observe(node):Pipe({
|
|
321
|
+
Rx.startFrom(function()
|
|
322
|
+
local currentIndex = self:_findNodeIndex(node)
|
|
323
|
+
if currentIndex then
|
|
324
|
+
return { currentIndex }
|
|
325
|
+
else
|
|
326
|
+
return {}
|
|
327
|
+
end
|
|
328
|
+
end);
|
|
329
|
+
})
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
--[=[
|
|
333
|
+
Gets the current index from the node
|
|
334
|
+
|
|
335
|
+
@param node SortedNode
|
|
336
|
+
@return number
|
|
337
|
+
]=]
|
|
338
|
+
function ObservableSortedList:GetIndexByKey(node)
|
|
339
|
+
assert(SortedNode.isSortedNode(node), "Bad node")
|
|
340
|
+
|
|
341
|
+
return self:_findNodeIndex(node)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
--[=[
|
|
345
|
+
Gets the count of items in the list
|
|
346
|
+
@return number
|
|
347
|
+
]=]
|
|
348
|
+
function ObservableSortedList:GetCount()
|
|
349
|
+
return self._countValue.Value or 0
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
--[=[
|
|
353
|
+
Gets a list of all entries.
|
|
354
|
+
@return { T }
|
|
355
|
+
]=]
|
|
356
|
+
function ObservableSortedList:GetList()
|
|
357
|
+
local list = table.create(self._countValue.Value)
|
|
358
|
+
for index, data in self:__iter() do
|
|
359
|
+
list[index] = data
|
|
360
|
+
end
|
|
361
|
+
return table.freeze(list)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
--[=[
|
|
365
|
+
Observes the count of the list
|
|
366
|
+
@return Observable<number>
|
|
367
|
+
]=]
|
|
368
|
+
function ObservableSortedList:ObserveCount()
|
|
369
|
+
return self._countValue:Observe()
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
--[=[
|
|
373
|
+
Adds the item to the list at the specified index
|
|
374
|
+
@param data T
|
|
375
|
+
@param observeValue Observable<Comparable> | Comparable
|
|
376
|
+
@return callback -- Call to remove
|
|
377
|
+
]=]
|
|
378
|
+
function ObservableSortedList:Add(data, observeValue)
|
|
379
|
+
assert(data ~= nil, "Bad data")
|
|
380
|
+
assert(Observable.isObservable(observeValue) or observeValue ~= nil, "Bad observeValue")
|
|
381
|
+
|
|
382
|
+
local node = SortedNode.new(data)
|
|
383
|
+
|
|
384
|
+
-- TODO: Store maid in node to prevent lookup of node -> index
|
|
385
|
+
local maid = Maid.new()
|
|
386
|
+
|
|
387
|
+
if Observable.isObservable(observeValue) then
|
|
388
|
+
maid:GiveTask(observeValue:Subscribe(function(sortValue)
|
|
389
|
+
self:_assignSortValue(node, sortValue)
|
|
390
|
+
end))
|
|
391
|
+
elseif observeValue ~= nil then
|
|
392
|
+
self:_assignSortValue(node, observeValue)
|
|
393
|
+
else
|
|
394
|
+
error("Bad observeValue")
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
maid:GiveTask(function()
|
|
398
|
+
-- TODO: Avoid cleaning up all these nodes when global maid cleans up
|
|
399
|
+
self:_assignSortValue(node, nil)
|
|
400
|
+
self._nodeIndexObservables:Complete(node)
|
|
401
|
+
end)
|
|
402
|
+
|
|
403
|
+
self._maid[node] = maid
|
|
404
|
+
|
|
405
|
+
return function()
|
|
406
|
+
self._maid[node] = nil
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
function ObservableSortedList:_assignSortValue(node, value)
|
|
411
|
+
if SortedNodeValue.isSortedNodeValue(node.value) then
|
|
412
|
+
if node.value:GetValue() == value then
|
|
413
|
+
return
|
|
414
|
+
end
|
|
415
|
+
elseif node.value == value then
|
|
416
|
+
return
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
if value == nil then
|
|
420
|
+
if self._root and self._root:ContainsNode(node) then
|
|
421
|
+
self._nodesRemoved[node] = true
|
|
422
|
+
self:_applyLowestIndexChanged(node:GetIndex())
|
|
423
|
+
self:_removeNode(node)
|
|
424
|
+
node.value = nil
|
|
425
|
+
self:_queueFireEvents()
|
|
426
|
+
else
|
|
427
|
+
node.value = nil
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
return
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
if self._compare ~= nil then
|
|
434
|
+
value = SortedNodeValue.new(value, self._compare)
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
-- our value changing didn't change anything
|
|
438
|
+
if not node:NeedsToMove(self._root, value) then
|
|
439
|
+
node.value = value
|
|
440
|
+
return
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
self._nodesRemoved[node] = nil
|
|
444
|
+
|
|
445
|
+
if self._root and self._root:ContainsNode(node) then
|
|
446
|
+
self:_applyLowestIndexChanged(node:GetIndex())
|
|
447
|
+
self:_removeNode(node)
|
|
448
|
+
else
|
|
449
|
+
self._nodesAdded[node] = true
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
node.value = value
|
|
453
|
+
|
|
454
|
+
self:_insertNode(node)
|
|
455
|
+
self:_applyLowestIndexChanged(node:GetIndex())
|
|
456
|
+
self:_queueFireEvents()
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
function ObservableSortedList:_applyLowestIndexChanged(index)
|
|
460
|
+
if self._lowestIndexChanged == nil then
|
|
461
|
+
self._lowestIndexChanged = index
|
|
462
|
+
return
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
if index < self._lowestIndexChanged then
|
|
466
|
+
self._lowestIndexChanged = index
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
function ObservableSortedList:_queueFireEvents()
|
|
471
|
+
if self._maid._fireEvents then
|
|
472
|
+
return
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
self._maid._fireEvents = task.defer(function()
|
|
476
|
+
self._maid._fireEvents = nil
|
|
477
|
+
self:_fireEvents()
|
|
478
|
+
end)
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
function ObservableSortedList:_fireEvents()
|
|
482
|
+
-- print(self._root)
|
|
483
|
+
|
|
484
|
+
local lowestIndexChanged = self._lowestIndexChanged
|
|
485
|
+
self._lowestIndexChanged = nil
|
|
486
|
+
|
|
487
|
+
local nodesAdded = self._nodesAdded
|
|
488
|
+
self._nodesAdded = {}
|
|
489
|
+
|
|
490
|
+
local nodesRemoved = self._nodesRemoved
|
|
491
|
+
self._nodesRemoved = {}
|
|
492
|
+
|
|
493
|
+
-- Fire count changed first
|
|
494
|
+
if self._root then
|
|
495
|
+
self._countValue.Value = self._root.descendantCount
|
|
496
|
+
else
|
|
497
|
+
self._countValue.Value = 0
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
-- TODO: Prevent Rx.of(itemAdded) stuff in our UI
|
|
501
|
+
for node in nodesAdded do
|
|
502
|
+
-- TODO: Prevent query slow here...?
|
|
503
|
+
local index = node:GetIndex()
|
|
504
|
+
self.ItemAdded:Fire(node.data, index, node)
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
for node in nodesRemoved do
|
|
508
|
+
self.ItemRemoved:Fire(node.data, node)
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
self.OrderChanged:Fire()
|
|
512
|
+
|
|
513
|
+
do
|
|
514
|
+
for index, node in self:_iterateNodesRange(lowestIndexChanged) do
|
|
515
|
+
-- TODO: Handle negative observations to avoid refiring upon insertion
|
|
516
|
+
-- TODO: Handle our state changing while we're firing
|
|
517
|
+
-- TODO: Avoid looping over nodes if we don't need to (track observations in node itself?)
|
|
518
|
+
local negative = ListIndexUtils.toNegativeIndex(self._root.descendantCount, index)
|
|
519
|
+
self._nodeIndexObservables:Fire(node, index)
|
|
520
|
+
self._indexObservers:Fire(index, node)
|
|
521
|
+
self._indexObservers:Fire(negative, node)
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
if self._mainObservables:HasSubscriptions("list") then
|
|
526
|
+
-- TODO: Reuse list
|
|
527
|
+
local list = self:GetList()
|
|
528
|
+
self._mainObservables:Fire("list", list)
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
function ObservableSortedList:_insertNode(node)
|
|
533
|
+
assert(SortedNode.isSortedNode(node), "Bad SortedNode")
|
|
534
|
+
|
|
535
|
+
if self._root == nil then
|
|
536
|
+
node:MarkBlack()
|
|
537
|
+
self._root = node
|
|
538
|
+
else
|
|
539
|
+
self._root = self._root:InsertNode(node)
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
function ObservableSortedList:_removeNode(nodeToRemove)
|
|
544
|
+
assert(SortedNode.isSortedNode(nodeToRemove), "Bad SortedNode")
|
|
545
|
+
|
|
546
|
+
if self._root ~= nil then
|
|
547
|
+
self._root = self._root:RemoveNode(nodeToRemove)
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
--[=[
|
|
552
|
+
Gets the current item at the index, or nil if it is not defined.
|
|
553
|
+
@param index number
|
|
554
|
+
@return T?
|
|
555
|
+
]=]
|
|
556
|
+
function ObservableSortedList:Get(index)
|
|
557
|
+
assert(type(index) == "number", "Bad index")
|
|
558
|
+
|
|
559
|
+
local node = self:_findNodeAtIndex(index)
|
|
560
|
+
if not node then
|
|
561
|
+
return nil
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
return node.data
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
--[=[
|
|
568
|
+
Removes the item from the list if it exists.
|
|
569
|
+
@param node SortedNode
|
|
570
|
+
@return T
|
|
571
|
+
]=]
|
|
572
|
+
function ObservableSortedList:RemoveByKey(node)
|
|
573
|
+
assert(SortedNode.isSortedNode(node), "Bad node")
|
|
574
|
+
|
|
575
|
+
self._maid[node] = nil
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
--[=[
|
|
579
|
+
Cleans up the ObservableSortedList and sets the metatable to nil.
|
|
580
|
+
]=]
|
|
581
|
+
function ObservableSortedList:Destroy()
|
|
582
|
+
self._maid:DoCleaning()
|
|
583
|
+
setmetatable(self, nil)
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
return ObservableSortedList
|
package/src/Shared/{ObservableSortedList.story.lua → SortedList/ObservableSortedList.story.lua}
RENAMED
|
@@ -19,11 +19,11 @@ return function(target)
|
|
|
19
19
|
|
|
20
20
|
local observableSortedList = maid:Add(ObservableSortedList.new())
|
|
21
21
|
|
|
22
|
-
local random = Random.new()
|
|
22
|
+
local random = Random.new(35)
|
|
23
23
|
|
|
24
24
|
local values = {}
|
|
25
25
|
for i=1, ENTRIES do
|
|
26
|
-
local scoreValue = maid:Add(ValueObject.new(0, "number"))
|
|
26
|
+
local scoreValue = maid:Add(ValueObject.new(0 or random:NextNumber(), "number"))
|
|
27
27
|
|
|
28
28
|
local data = {
|
|
29
29
|
originalIndex = i;
|
|
@@ -105,6 +105,7 @@ return function(target)
|
|
|
105
105
|
};
|
|
106
106
|
|
|
107
107
|
Blend.New "TextLabel" {
|
|
108
|
+
Name = "Score";
|
|
108
109
|
Text = data.scoreValue:Observe():Pipe({
|
|
109
110
|
Rx.map(tostring)
|
|
110
111
|
});
|
|
@@ -116,17 +117,8 @@ return function(target)
|
|
|
116
117
|
TextXAlignment = Enum.TextXAlignment.Left;
|
|
117
118
|
};
|
|
118
119
|
|
|
119
|
-
Blend.New "TextLabel" {
|
|
120
|
-
Text = data.originalIndex;
|
|
121
|
-
Size = UDim2.fromScale(1, 1);
|
|
122
|
-
BackgroundTransparency = 1;
|
|
123
|
-
Position = UDim2.new(0, -10, 0.5, 0);
|
|
124
|
-
AnchorPoint = Vector2.new(1, 0.5);
|
|
125
|
-
TextColor3 = Color3.new(1, 1, 1);
|
|
126
|
-
TextXAlignment = Enum.TextXAlignment.Right;
|
|
127
|
-
};
|
|
128
|
-
|
|
129
120
|
Blend.New "TextBox" {
|
|
121
|
+
Name = "SetScore";
|
|
130
122
|
Size = UDim2.fromScale(1, 1);
|
|
131
123
|
Text = tostring(data.scoreValue.Value);
|
|
132
124
|
BackgroundTransparency = 1;
|
|
@@ -139,6 +131,17 @@ return function(target)
|
|
|
139
131
|
end
|
|
140
132
|
end;
|
|
141
133
|
};
|
|
134
|
+
|
|
135
|
+
Blend.New "TextLabel" {
|
|
136
|
+
Name = "OriginalIndex";
|
|
137
|
+
Text = data.originalIndex;
|
|
138
|
+
Size = UDim2.fromScale(1, 1);
|
|
139
|
+
BackgroundTransparency = 1;
|
|
140
|
+
Position = UDim2.new(0, -10, 0.5, 0);
|
|
141
|
+
AnchorPoint = Vector2.new(1, 0.5);
|
|
142
|
+
TextColor3 = Color3.new(1, 1, 1);
|
|
143
|
+
TextXAlignment = Enum.TextXAlignment.Right;
|
|
144
|
+
};
|
|
142
145
|
}
|
|
143
146
|
end)
|
|
144
147
|
})
|