-
49 |
-
50 | 51 | 52 | 53 |54 |
55 |
├── .gitignore ├── list ├── test.html ├── benchmark.html ├── list_test.js ├── list.js └── list_benchmark.js ├── .travis.yml ├── can-derive.js ├── examples └── filter │ ├── style.css │ ├── perf-can-derive.html │ ├── perf-can.html │ ├── perf-react.html │ ├── todomvc.html │ ├── test-drive.html │ └── common.js ├── .jshintrc ├── LICENSE.md ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ -------------------------------------------------------------------------------- /list/test.html: -------------------------------------------------------------------------------- 1 |
44 | Mark a todo incompleted. Do you notice the delay? 45 |
46 |
210 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # can-derive
2 |
3 | [](https://travis-ci.org/canjs/can-derive)
4 |
5 | **can-derive** is a plugin that creates observable filtered lists that remain
6 | in sync with their original source list.
7 |
8 | For example, a todo list might contain todo objects with a `completed` property.
9 | Traditionally `can.List.filter` enables you to create a new `can.List`
10 | containing only the "completed" todo objects. However, if the source list were
11 | to change in any way - for instance via an "add" or "remove" - the returned
12 | `can.List` may become an innaccurate representation of the source list.
13 | The same filtered list of "completed" todo objects created
14 | with `can-derive`'s `can.List.dFilter` would always be an accurate
15 | representation of with the source list regardless of how it was manipulated.
16 |
17 | **can-derive** is ideal for cases where the source list contains at least
18 | 10 items and is expected to be "changed" frequently (3 or more times).
19 |
20 | Check out the demo.
21 |
22 |
23 |
24 |
25 |
26 | - [Install](#install)
27 | - [Use](#use)
28 | - [With can.Map.define](#with-canmapdefine)
29 | - [Accessing FilteredList values](#accessing-filteredlist-values)
30 | - [API](#api)
31 | - [can.List](#canlist)
32 | - [.dFilter()](#dfilter)
33 | - [FilteredList](#filteredlist)
34 | - [Inherited can.RBTreeList methods](#inherited-canrbtreelist-methods)
35 | - [Disabled can.RBTreeList methods](#disabled-canrbtreelist-methods)
36 | - [Performance](#performance)
37 | - [When to Use](#when-to-use)
38 | - [Contributing](#contributing)
39 |
40 |
41 |
42 | ## Install
43 |
44 | Use npm to install `can-derive`:
45 |
46 | ```
47 | npm install can-derive --save
48 | ```
49 |
50 | ## Use
51 |
52 | Use `require` in Node/Browserify workflows to import the `can-derive` plugin
53 | like:
54 |
55 | ```js
56 | require('can');
57 | require('can-derive');
58 | ```
59 |
60 | Use `define`, `require`, or `import` in [StealJS](http://stealjs.com/) workflows
61 | to import the `can-derive` plugin like:
62 |
63 | ```js
64 | import 'can';
65 | import 'can-derive';
66 | ```
67 |
68 | Once you've imported `can-derive` into your project use
69 | `can.List.dFilter` to generate a derived list based on a `predicate` function.
70 | The following example derives a list of "completed" items from a `can.List`
71 | of `todo` objects:
72 |
73 | ```js
74 | var todos = new can.List([
75 | { name: 'Hop', complete: true },
76 | { name: 'Skip', complete: false },
77 | //...
78 | ]);
79 |
80 | var completed = todos.dFilter(function(todo) {
81 | return todo.attr("complete") === true;
82 | });
83 | ```
84 |
85 | Any changes to `todos` will be propagated to the derived `completed`
86 | list:
87 |
88 | ```js
89 | completed.bind('add', function(ev, newItems) {
90 | console.log(newItems.length, 'item(s) added');
91 | });
92 |
93 | todos.push({ name: 'Jump', complete: true },
94 | { name: 'Sleep', complete: false }); //-> "1 item(s) added"
95 | ```
96 |
97 | ### With can.Map.define
98 |
99 | If you're using the [can.Map.define
100 | plugin](http://canjs.com/docs/can.Map.prototype.define.html), you can define a
101 | derived list like so:
102 |
103 | ```js
104 | {
105 | define: {
106 | todos: {
107 | Value: can.List
108 | },
109 | completedTodos: {
110 | get: function() {
111 | return this.attr('todos').dFilter(function(todo){
112 | return todo.attr('complete') === true;
113 | });
114 | }
115 | }
116 | }
117 | }
118 | ```
119 |
120 | Note: The `can-derive` plugin ensures that the define plugin's `get` method will
121 | not observe "length" like it would a traditional [can.List](http://canjs.com/docs/can.List.html)
122 | when calling `.filter()`.
123 |
124 |
125 | ### Accessing FilteredList values
126 |
127 | Unlike `can.List` and `Array`, indexes of a `FilteredList` **cannot** be
128 | accessed using bracket notation:
129 |
130 | ```js
131 | filteredList[1]; //-> undefined
132 | ```
133 |
134 | To access a `FilteredList`'s values, use [`.attr()`](https://github.com/canjs/can-binarytree#attr):
135 |
136 | ```js
137 | filteredList.attr(); //-> ["a", "b", "c"]
138 | filteredList.attr(0); //-> "a"
139 | filteredList.attr(1); //-> "b"
140 | filteredList.attr(2); //-> "c"
141 | filteredList.attr('length'); //-> 3
142 | ```
143 |
144 | This is due to the fact that a `FilteredList` inherits a [`can.RBTreeList`](https://github.com/canjs/can-binarytree#canrbtreelist)
145 | which stores its values in a [Red-black tree](https://en.wikipedia.org/wiki/Red%E2%80%93black_tree)
146 | for [performance](#performance) - rather than a series of numeric keys.
147 |
148 |
149 | ## API
150 |
151 | ### can.List
152 |
153 | #### .dFilter()
154 |
155 | `sourceList.filter(predicateFn) -> FilteredList`
156 |
157 | Similar to [`.filter()`](https://github.com/canjs/can-derive#filter) except
158 | that the returned `FilteredList` is bound to `sourceList`.
159 |
160 | Returns a `FilteredList`.
161 |
162 | ### FilteredList
163 |
164 | #### Inherited can.RBTreeList methods
165 |
166 | Since `FilteredList` inherits from [can.RBTreeList](https://github.com/canjs/can-binarytree#canrbtreelist),
167 | the following methods are available:
168 |
169 | - [`.attr()`](https://github.com/canjs/can-binarytree#attr)
170 | - [`.each()`](https://github.com/canjs/can-binarytree#each)
171 | - [`.eachNode()`](https://github.com/canjs/can-binarytree#eachnode)
172 | - [`.filter()`](https://github.com/canjs/can-binarytree#filter)
173 | - [`.indexOf()`](https://github.com/canjs/can-binarytree#indexof)
174 | - [`.indexOfNode()`](https://github.com/canjs/can-binarytree#indexofnode)
175 | - [`.map()`](https://github.com/canjs/can-binarytree#map)
176 | - `.slice()` *(coming soon)*
177 |
178 | #### Disabled can.RBTreeList methods
179 |
180 | A `FilteredList` is bound to its source list and manipulted as it changes.
181 | Because of this, it is read-only and the following `can.RBTreeList`
182 | methods are disabled:
183 |
184 | - `.push()`
185 | - `.pop()`
186 | - `.removeAttr()`
187 | - `.replace()`
188 | - `.shift()`
189 | - `.splice()`
190 | - `.unshift()`
191 |
192 | ## Performance
193 |
194 | `can-derive` optimizes for insertions and removals, completing them in `O(log n)`
195 | time. This means that changes to the source list will automatically update the
196 | derived list in `O(log n)` time, compared to the standard `O(n)` time you would
197 | expect in other implementations.
198 |
199 | It does this by:
200 |
201 | - Keeping the derived list in a [Red-black tree](https://en.wikipedia.org/wiki/Red%E2%80%93black_tree)
202 | modeled after an `Array`
203 | - Listening for additions or removals in the source list
204 | - Listening for predicate function result changes for any item
205 |
206 | This algorithm was originally discussed in [this StackExchange
207 | thread](http://cs.stackexchange.com/questions/43447/order-preserving-update-of-a
208 | -sublist-of-a-list-of-mutable-objects-in-sublinear-t/44502#44502).
209 |
210 | ### When to Use
211 |
212 | In general, it is preferable to use `can-derive` over alternative approaches
213 | when:
214 |
215 | - Your source list contains 10 or more elements
216 | - You need to know how the filtered list changed, for instance when rendering
217 | in the DOM.
218 |
219 |
220 | ## Contributing
221 |
222 | To set up your dev environment:
223 |
224 | 1. Clone and fork this repo.
225 | 2. Run `npm install`.
226 | 3. Open `list/test.html` in your browser. Everything should pass.
227 | 4. Run `npm test`. Everything should pass.
228 | 5. Run `npm run-script build`. Everything should build ok
229 |
--------------------------------------------------------------------------------
/list/list_test.js:
--------------------------------------------------------------------------------
1 | var QUnit = require("steal-qunit");
2 | require("./list");
3 | require('can/map/define/define');
4 |
5 | QUnit.module('.dFilter()', {
6 | setup: function () {}
7 | });
8 |
9 | var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
10 | var dirtyAlphabet = letters.split('');
11 | dirtyAlphabet.splice(2, 0, 0);
12 | dirtyAlphabet.splice(5, 0, 0);
13 | dirtyAlphabet.splice(12, 0, 0);
14 | dirtyAlphabet.splice(16, 0, 0);
15 | dirtyAlphabet.splice(22, 0, 0);
16 | dirtyAlphabet.splice(27, 0, 0);
17 |
18 | var equalValues = function (list, expectedValues) {
19 | var match = true;
20 |
21 | if (list.length !== expectedValues.length) {
22 | return false;
23 | }
24 |
25 | list.each(function (item, index) {
26 | if (item !== expectedValues[index]) {
27 | match = false;
28 | }
29 | });
30 |
31 | return match;
32 | };
33 |
34 | test('Method exists', function () {
35 | ok(can.List.prototype.dFilter, 'List has dFilter method');
36 | });
37 |
38 | test('The predicate function is executed when a source item changes', function () {
39 |
40 | var values = [true, true, true];
41 |
42 | var source = new can.List(values);
43 | var predicateFn = function (item) {
44 | return item;
45 | };
46 | var derived = source.dFilter(predicateFn);
47 |
48 | equal(derived.attr('length'), 3, 'Initial length is correct');
49 |
50 | derived.predicate = function () {
51 | ok(true, 'Predicate should be run for source changes');
52 | return predicateFn.apply(this, arguments);
53 | };
54 |
55 | source.attr(1, false);
56 |
57 | equal(derived.attr('length'), 2, 'Item removed after source change');
58 | });
59 |
60 | test('Derives initial values', function () {
61 |
62 | var filterFn = function (value, index) { return value ? true : false; };
63 | var source = new can.List(dirtyAlphabet);
64 | var expected = dirtyAlphabet.filter(filterFn);
65 | var derived = source.dFilter(filterFn);
66 |
67 | ok(equalValues(derived, expected), 'Initial values are correct');
68 | });
69 |
70 | test('Changes to source list are synced to their derived list', function () {
71 |
72 | var alphabet = dirtyAlphabet.slice();
73 | var filterFn = function (value) { return value ? true : false; };
74 | var source = new can.List(alphabet);
75 |
76 | var derived = source.dFilter(filterFn);
77 | var expected;
78 |
79 | source.attr(4, 'DD'); // D -> DD
80 | alphabet[4] = 'DD'; // Update static list
81 | expected = alphabet.filter(filterFn);
82 |
83 | ok(equalValues(derived, expected), 'Set derived'); // Compare
84 |
85 | source.attr(10, 'II'); // I -> II
86 | alphabet[10] = 'II';
87 | expected = alphabet.filter(filterFn);
88 |
89 | ok(equalValues(derived, expected), 'Set derived');
90 |
91 | source.attr(29, 'XX'); // X -> XX
92 | alphabet[29] = 'XX';
93 | expected = alphabet.filter(filterFn);
94 |
95 | ok(equalValues(derived, expected), 'Set derived');
96 | });
97 |
98 | test('Items added to a source list get added to their derived list', function () {
99 |
100 | var alphabet = dirtyAlphabet.slice();
101 | var filterFn = function (value) { return value ? true : false; };
102 | var source = new can.List(alphabet);
103 | var derived = source.dFilter(filterFn);
104 | var expected;
105 |
106 | derived.bind('add', function (ev, items, offset) {
107 | items.forEach(function (item, index) {
108 | equal(item, expected[offset + index],
109 | 'Add event reports correct value/index');
110 | });
111 | });
112 |
113 | // Insert before
114 | alphabet.unshift('Aey');
115 | expected = alphabet.filter(filterFn);
116 | source.unshift('Aey');
117 |
118 | ok(equalValues(derived, expected), 'Item added via .unshift()');
119 |
120 | // Insert between
121 | alphabet.splice(20, 0, 'Ohh');
122 | expected = alphabet.filter(filterFn);
123 | source.splice(20, 0, 'Ohh');
124 |
125 | ok(equalValues(derived, expected), 'Item added via .splice()');
126 |
127 | // Insert after
128 | alphabet.push('Zee');
129 | expected = alphabet.filter(filterFn);
130 | source.push('Zee');
131 |
132 | ok(equalValues(derived, expected), 'Item added via .push()');
133 | });
134 |
135 | test('Items removed from a source list are removed from their derived list', function () {
136 | var alphabet = dirtyAlphabet.slice();
137 | var filterFn = function (value) { return value ? true : false; };
138 | var source = new can.List(alphabet);
139 | var derived = source.dFilter(filterFn);
140 | var expected;
141 |
142 | // Remove first
143 | source.shift();
144 | alphabet.shift();
145 | expected = alphabet.filter(filterFn);
146 |
147 | ok(equalValues(derived, expected), 'Item removed via .shift()');
148 |
149 | // Remove middle
150 | source.splice(10, 1);
151 | alphabet.splice(10, 1);
152 | expected = alphabet.filter(filterFn);
153 |
154 | ok(equalValues(derived, expected), 'Item removed via .splice()');
155 |
156 | // Remove last
157 | source.pop();
158 | alphabet.pop();
159 | expected = alphabet.filter(filterFn);
160 |
161 | ok(equalValues(derived, expected), 'Item removed via .pop()');
162 | });
163 |
164 | test('Predicate function can be bound to source index', function () {
165 | var alphabet = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
166 | var filterFn = function (value, index) { return index % 2 === 0; };
167 | var source = new can.List(alphabet);
168 | var derived = source.dFilter(filterFn);
169 | var expected = alphabet.filter(filterFn);
170 |
171 | // Initial values
172 | ok(equalValues(derived, expected), 'Odd indexed items excluded');
173 |
174 | // Insert at the beginning
175 | source.unshift('_a');
176 | alphabet.unshift('_a');
177 | expected = alphabet.filter(filterFn);
178 |
179 | ok(equalValues(derived, expected), 'Items are flopped after an insert at the beginning');
180 |
181 | // Remove from the beginning
182 | source.shift();
183 | alphabet.shift();
184 | expected = alphabet.filter(filterFn);
185 |
186 | ok(equalValues(derived, expected), 'Items are flopped after a remove at the beginning');
187 |
188 | // Insert near the middle
189 | // NOTE: Make sure this happens at an even index
190 | source.splice(2, 0, 'b <-> c');
191 | alphabet.splice(2, 0, 'b <-> c');
192 | expected = alphabet.filter(filterFn);
193 |
194 | ok(equalValues(derived, expected), 'Segment of items are flopped after an insert (in the middle)');
195 |
196 | // Remove from the middle
197 | source.splice(11, 1);
198 | alphabet.splice(11, 1);
199 | expected = alphabet.filter(filterFn);
200 |
201 | ok(equalValues(derived, expected), 'Segment of items are flopped after a remove (in the middle)');
202 |
203 | // Replace in the middle
204 | source.splice(10, 1, '10B');
205 | alphabet.splice(10, 1, '10B');
206 | expected = alphabet.filter(filterFn);
207 |
208 | ok(equalValues(derived, expected), 'Items are mostly unchanged after a replace');
209 |
210 | // Add at the end
211 | source.push('ZZZ');
212 | alphabet.push('ZZZ');
213 | expected = alphabet.filter(filterFn);
214 |
215 | ok(equalValues(derived, expected), 'Item is added at the end');
216 |
217 | // Remove at the end
218 | source.pop();
219 | alphabet.pop();
220 | expected = alphabet.filter(filterFn);
221 |
222 | ok(equalValues(derived, expected), 'Item is removed from the end');
223 |
224 | });
225 |
226 | test('Can derive a filtered list from a filtered list', function () {
227 | var letterCollection = [];
228 | var total = 4; // 40, because it's evenly divisible by 4
229 |
230 | // Generate values
231 | for (var i = 0; i < total; i++) {
232 | letterCollection.push(letters[i]);
233 | }
234 |
235 | // Half filters
236 | var makeFilterFn = function (predicate) {
237 | return function (value, index, collection) {
238 |
239 | // Handle can.List's and native Array's
240 | var length = collection.attr ?
241 | collection.attr('length') :
242 | collection.length;
243 |
244 | var middleIndex = Math.round(length / 2);
245 |
246 | return predicate(index, middleIndex);
247 | };
248 | };
249 |
250 | var firstHalfFilter = makeFilterFn(function (a, b) { return a < b; });
251 | var secondHalfFilter = makeFilterFn(function (a, b) { return a >= b; });
252 |
253 | var source = new can.List(letterCollection);
254 |
255 | // Filter the whole collection into two separate lists
256 | var derivedFirstHalf = source.dFilter(firstHalfFilter);
257 | var derivedSecondHalf = source.dFilter(secondHalfFilter);
258 |
259 | // Filter the two lists into four additional lists
260 | var derivedFirstQuarter = derivedFirstHalf.dFilter(firstHalfFilter);
261 | var derivedSecondQuarter = derivedFirstHalf.dFilter(secondHalfFilter);
262 | var derivedThirdQuarter = derivedSecondHalf.dFilter(firstHalfFilter);
263 | var derivedFourthQuarter = derivedSecondHalf.dFilter(secondHalfFilter);
264 |
265 | var evaluate = function () {
266 | // Recreate the halfed/quartered lists using native .dFilter()
267 | var expectedFirstHalf = letterCollection.filter(firstHalfFilter);
268 | var expectedSecondHalf = letterCollection.filter(secondHalfFilter);
269 | var expectedFirstQuarter = expectedFirstHalf.filter(firstHalfFilter);
270 | var expectedSecondQuarter = expectedFirstHalf.filter(secondHalfFilter);
271 | var expectedThirdQuarter = expectedSecondHalf.filter(firstHalfFilter);
272 | var expectedFourthQuarter = expectedSecondHalf.filter(secondHalfFilter);
273 |
274 | ok(equalValues(derivedFirstHalf, expectedFirstHalf), '1st half matches expected');
275 | ok(equalValues(derivedSecondHalf, expectedSecondHalf), '2nd half matches expected');
276 | ok(equalValues(derivedFirstQuarter, expectedFirstQuarter), '1st quarter matches expected');
277 | ok(equalValues(derivedSecondQuarter, expectedSecondQuarter), '2nd quarter matches expected');
278 | ok(equalValues(derivedThirdQuarter, expectedThirdQuarter), '3rd quarter matches expected');
279 | ok(equalValues(derivedFourthQuarter, expectedFourthQuarter), '4th quarter matches expected');
280 | };
281 |
282 | // Initial values
283 | evaluate();
284 |
285 | // Insert
286 | source.push(letters[total]);
287 | letterCollection.push(letters[total]);
288 | evaluate();
289 |
290 | // Remove
291 | source.shift();
292 | letterCollection.shift();
293 | evaluate();
294 | });
295 |
296 | test('Derived list fires add/remove/length events', function () {
297 | var filterFn = function (value, index) {
298 | return value ? true : false;
299 | };
300 | var alphabet = dirtyAlphabet.slice();
301 | var source = new can.List(dirtyAlphabet);
302 | var derived = source.dFilter(filterFn);
303 | var expected = alphabet.filter(filterFn);
304 |
305 | derived.bind('add', function (ev, added, offset) {
306 | ok(true, '"add" event fired');
307 | // NOTE: Use deepEqual to compare values, not list instances
308 | deepEqual(added, ['ZZ'], 'Correct newVal passed to "add" handler');
309 | });
310 |
311 | derived.bind('remove', function (ev, removed, offset) {
312 | ok(true, '"remove" event fired');
313 | // NOTE: Use deepEqual to compare values, not list instances
314 | deepEqual(removed, ['D'], 'Correct oldVal passed to "remove" handler');
315 | });
316 |
317 | derived.bind('length', function (ev, newVal) {
318 | ok('"length" event fired');
319 | equal(newVal, expected.length, 'Correct newVal passed to "length" handler');
320 | });
321 |
322 | // Add
323 | alphabet.splice(alphabet.length - 1, 0, 'ZZ');
324 | expected = alphabet.filter(filterFn);
325 | source.splice(source.length - 1, 0, 'ZZ');
326 |
327 | // Remove
328 | alphabet.splice(4, 1);
329 | expected = alphabet.filter(filterFn);
330 | source.splice(4, 1);
331 | });
332 |
333 | test('Can iterate initial values with .each()', function () {
334 | var filterFn = function (value, index) {
335 | return value ? true : false;
336 | };
337 | var source = new can.List(dirtyAlphabet);
338 | var expected = dirtyAlphabet.filter(filterFn);
339 | var derived = source.dFilter(filterFn);
340 |
341 | derived.each(function (value, index) {
342 | equal(value, expected[index], 'Iterated value matches expected value');
343 | });
344 | });
345 |
346 | test('.attr([index]) returns correct values', function () {
347 | var filterFn = function (value, index) {
348 | return value ? true : false;
349 | };
350 | var source = new can.List(dirtyAlphabet);
351 | var expected = dirtyAlphabet.filter(filterFn);
352 | var derived = source.dFilter(filterFn);
353 |
354 | expected.forEach(function (expectedValue, index) {
355 | equal(derived.attr(index), expectedValue, 'Read value matches expected value');
356 | });
357 | });
358 |
359 | test('Predicate function can be passed an object and call a function', function () {
360 | var source = new can.List();
361 | var predicateFn = function (value) {
362 | var result = value.fullName() === FILTERED_VALUE.fullName();
363 | return result;
364 | };
365 | var value;
366 |
367 | for (var i = 0; i < 10; i++) {
368 | value = new can.Map({
369 | id: i.toString(16),
370 | firstName: 'Chris',
371 | lastName: 'Gomez',
372 | fullName: function () {
373 | return this.attr('firstName') + ' ' + this.attr('lastName') +
374 | '_' + this.attr('id');
375 | }
376 | });
377 | source.push(value);
378 | }
379 |
380 | var FILTERED_VALUE = value;
381 | var derived = source.dFilter(predicateFn);
382 |
383 | equal(derived.attr('length'), 1, 'Length is correct after initial filter');
384 |
385 | source.attr(0, FILTERED_VALUE);
386 |
387 | equal(derived.attr('length'), 2, 'Length is correct after set');
388 |
389 | source.attr('5.id', FILTERED_VALUE.id);
390 |
391 | equal(derived.attr('length'), 3, 'Length is correct after change');
392 | });
393 |
394 | test('Get value at index using attr()', function () {
395 | var source = new can.List(['a', 'b', 'c']);
396 | var derived = source.dFilter(function () {
397 | return true;
398 | });
399 |
400 | equal(derived.attr(0), 'a', 'Got value using .attr()');
401 | equal(derived.attr(1), 'b', 'Got value using .attr()');
402 | equal(derived.attr(2), 'c', 'Got value using .attr()');
403 | });
404 |
405 | test('Emptying a source tree emtpies its filtered tree', function () {
406 | var source = new can.List(['a', 'b', 'c', 'd', 'e', 'f']);
407 | var filtered = source.dFilter(function () { return true; });
408 |
409 | source.splice(0, source.length);
410 |
411 | equal(source.length, 0, 'Tree is empty');
412 | equal(filtered.length, 0, 'Tree is empty');
413 | });
414 |
415 | test('Can be used inside a define "get" method', function () {
416 |
417 | var expectingGet = true;
418 |
419 | var Map = can.Map.extend({
420 | define: {
421 | todos: {
422 | value: function () {
423 | return new can.List([
424 | { name: 'Hop', completed: true },
425 | { name: 'Skip', completed: false },
426 | { name: 'Jump', completed: true }
427 | ]);
428 | }
429 | },
430 | completed: {
431 | get: function () {
432 | if (! expectingGet) {
433 | ok(false, '"get" method called unexpectedly');
434 | }
435 |
436 | expectingGet = false;
437 |
438 | return this.attr('todos').dFilter(function (todo) {
439 | return todo.attr('completed') === true;
440 | });
441 | }
442 | }
443 | }
444 | });
445 |
446 | var map = new Map();
447 |
448 | // Enable caching of virtual properties
449 | map.bind('completed', can.noop);
450 |
451 | var completed = map.attr('completed');
452 |
453 | map.attr('todos').push({ name: 'Pass test', completed: true });
454 |
455 | ok(completed === map.attr('completed'),
456 | 'Derived list instance is the same');
457 |
458 | expectingGet = true;
459 |
460 | map.attr('todos', new can.List([
461 | { name: 'Drop mic', completed: true }
462 | ]));
463 |
464 | ok(completed !== map.attr('completed'),
465 | 'Derived list instance has changed');
466 | });
467 |
468 | test('Returned list is read-only', function () {
469 | var list = new can.List(['a', 'b', 'c']);
470 | var filtered = list.dFilter(function (value) {
471 | return value === 'b';
472 | });
473 | var expectedLength = filtered.attr('length');
474 |
475 | filtered.push({ foo: 'bar'});
476 | equal(filtered.attr('length'), expectedLength, '.push() had no effect');
477 |
478 | filtered.pop();
479 | equal(filtered.attr('length'), expectedLength, '.pop() had no effect');
480 |
481 | filtered.shift();
482 | equal(filtered.attr('length'), expectedLength, '.shift() had no effect');
483 |
484 | filtered.unshift({ yo: 'ho' });
485 | equal(filtered.attr('length'), expectedLength, '.unshift() had no effect');
486 |
487 | filtered.splice(0, 1);
488 | equal(filtered.attr('length'), expectedLength, '.splice() had no effect');
489 |
490 | filtered.replace(['a', 'b', 'c']);
491 | equal(filtered.attr('length'), expectedLength, '.replace() had no effect');
492 |
493 | });
494 |
495 | test('Derived list can be unbound from source', function () {
496 | var list = new can.List(['a', 'b', 'c']);
497 | var filtered = list.dFilter(function (value) {
498 | return value === 'b';
499 | });
500 |
501 | equal(list._bindings, 2, 'Derived list is bound to source list');
502 |
503 | // Unbind the derived list from the source (we're not concerned
504 | // about the filtered list being bound to the derived list)
505 | filtered._source.unbindFromSource();
506 |
507 | equal(list._bindings, 0,
508 | 'Derived list is not bound to the source list');
509 | equal(list._derivedList, undefined,
510 | 'Source list has no reference to the derived list');
511 | });
--------------------------------------------------------------------------------
/list/list.js:
--------------------------------------------------------------------------------
1 | var RBTreeList = require('can-binarytree').RBTreeList;
2 | require('can/list/list');
3 | require('can/compute/compute');
4 | require('can/util/util');
5 |
6 | var __observe = can.__observe;
7 | var __observeAbstractValues = false;
8 | var _triggerChange, __observeException, __predicateObserve,
9 | DerivedList, FilteredList, ObservedPredicate;
10 |
11 |
12 | // Dispatch a `__modified` event alongside all other `can.Map` events as
13 | // a non-recursive alternative to `change` events
14 | _triggerChange = can.Map.prototype._triggerChange;
15 | can.Map.prototype._triggerChange = function (attr, how, newVal, oldVal) {
16 | _triggerChange.apply(this, arguments);
17 |
18 | can.batch.trigger(this, {
19 | type: '__modified',
20 | target: this
21 | }, [newVal, oldVal]);
22 | };
23 |
24 | // Create an observe function that can be configured remotely to bind
25 | // differently to maps and children of the source list
26 | __predicateObserve = function (obj, event) {
27 | if (obj === __observeException) {
28 | return;
29 | }
30 |
31 | if (__observeAbstractValues && ! (obj instanceof can.List) &&
32 | obj instanceof can.Map) {
33 | event = '__modified';
34 | }
35 |
36 | return __observe.call(this, obj, event);
37 | };
38 |
39 | var eachNodesOrItems = function (source, iterator, context) {
40 | if (source instanceof can.RBTreeList) {
41 | return source.eachNode(iterator, context);
42 | } else {
43 | return can.each.apply(can.each, arguments);
44 | }
45 | };
46 |
47 | // Use a tree so that items are sorted by the source list's
48 | // index in O(log(n)) time
49 | DerivedList = RBTreeList.extend({
50 |
51 | // A flag that determines if index influencing operations like shift
52 | // and splice should result in O(n) predicate evaluations
53 | _indexBound: false,
54 |
55 | dFilter: function (predicate, predicateContext) {
56 | var context = this;
57 | var filteredList;
58 |
59 | can.__notObserve(function () {
60 |
61 | if (! context._derivedList) {
62 | context._derivedList = new DerivedList(context);
63 | }
64 |
65 | filteredList = new FilteredList(context._derivedList, predicate,
66 | predicateContext);
67 |
68 | // Set _indexBound to true if this filtered list depends on the
69 | // index. Once set to true there's no going back.
70 | if (! context._derivedList._indexBound &&
71 | filteredList._indexBound) {
72 | context._derivedList._indexBound = true;
73 | }
74 |
75 | })();
76 |
77 | return filteredList;
78 | },
79 |
80 | setup: function () {
81 |
82 | var setupResult = RBTreeList.prototype.setup.apply(this, arguments);
83 |
84 | // CanJS 3.0
85 | if (this.___get) {
86 | this.___get = this.____get;
87 |
88 | // CanJS 2.2.9
89 | } else {
90 | this.__get = this.____get;
91 | }
92 |
93 | return setupResult;
94 | },
95 |
96 | init: function (sourceList, initializeWithoutItems) {
97 |
98 | var context = this;
99 | var initArgs = [];
100 | var initializeWithItems = !initializeWithoutItems;
101 |
102 | // Save a reference to the list we're deriving
103 | this._source = sourceList;
104 |
105 | // Don't populate the tree with the items initially passed
106 | // to the constructor
107 | if (initializeWithItems) {
108 | var initialItems = [];
109 |
110 | // `sourceList` can be either a native JS array, a `can.List, or
111 | // a `can.RBTreeList`, thus the `can.each` and not
112 | // `sourceList.each`
113 | can.each(sourceList, function (value, index) {
114 | initialItems[index] = context.describeSourceItem(value, index);
115 | });
116 |
117 | initArgs[0] = initialItems;
118 | initArgs[1] = function (index, node) {
119 | initialItems[index].node = node;
120 | };
121 | }
122 |
123 | // Setup the tree
124 | RBTreeList.prototype.init.apply(this, initArgs);
125 |
126 | // Make this list a reflection of the source list
127 | this.syncAdds(! initializeWithItems);
128 | this.syncRemoves();
129 | this.syncValues();
130 | },
131 |
132 | unbindFromSource: function () {
133 | this._source.unbind('add', this._addHandler);
134 | this._source.unbind('remove', this._removeHandler);
135 | delete this._source._derivedList;
136 | },
137 |
138 | syncAdds: function (addInitialItems) {
139 |
140 | if (addInitialItems) {
141 | this.addItems(this._source, 0);
142 | }
143 |
144 | this._addHandler = can.proxy(function (ev, items, offset) {
145 | this.addItems(items, offset);
146 | }, this);
147 |
148 | // Add future items
149 | this._source.bind('add', this._addHandler);
150 | },
151 |
152 | syncRemoves: function () {
153 |
154 | this._removeHandler = can.proxy(function (ev, items, offset) {
155 | this.removeItems(items, offset);
156 | }, this);
157 |
158 | // Remove future items
159 | this._source.bind('remove', this._removeHandler);
160 | },
161 |
162 |
163 | syncValues: function () {
164 |
165 | var tree = this;
166 |
167 | // Handle the re-assigment of index values
168 | var ___set = this._source.___set;
169 | this._source.___set = function (index, value) {
170 |
171 | var node = tree.get(index);
172 |
173 | if (node) {
174 | node.data.index = index;
175 | node.data.value = value;
176 | can.batch.trigger(tree, '__nodeModified', [node]);
177 | }
178 |
179 | // Continue the `set` on the source list
180 | return ___set.apply(this, arguments);
181 | };
182 | },
183 |
184 | addItems: function (items, offset) {
185 | var self = this;
186 |
187 | eachNodesOrItems(items, function (item, i) {
188 | self.addItem(item, offset + i);
189 | });
190 | },
191 |
192 | addItem: function (item, insertIndex) {
193 | var context = this;
194 | var sourceItem = this.describeSourceItem.apply(this, arguments);
195 | var node;
196 |
197 | // Don't dispatch the resulting "add" event until a reference
198 | // to the node has been saved to the `sourceItem` object
199 | can.batch.start();
200 |
201 | node = this.set(insertIndex, sourceItem, true);
202 | sourceItem.node = node;
203 |
204 | // Deal with the items after this inserted item that now
205 | // have a new index
206 | context._propagateIndexShift(insertIndex + 1);
207 |
208 | // Stop batching once all of the events have been queued
209 | can.batch.stop();
210 | },
211 |
212 | describeSourceItem: function (item, insertIndex) {
213 | var tree = this;
214 |
215 | // Store information in a way that changes can be bound to
216 | var sourceItem = {};
217 | sourceItem.index = insertIndex;
218 | sourceItem.value = item;
219 |
220 | if (item.bind) {
221 | item.bind('__modified', function () {
222 | can.batch.trigger(tree, '__nodeModified', [sourceItem.node]);
223 | });
224 | }
225 |
226 | return sourceItem;
227 | },
228 |
229 | _propagateIndexShift: function (affectedIndex) {
230 |
231 | var i, node;
232 |
233 | // When the `_indexBound` flag is true that means that a predicate
234 | // function of one of the filtered lists that use this derived list
235 | // as their source is bound to the index. This is unfortunate,
236 | // because now we have to manually update a compute that stores the
237 | // index so that the filtered list that is bound to the index can
238 | // re-run its predicate function for all of the items whos indices
239 | // have changed. Which of course now makes this an O(n) filter. And
240 | // worse, this will apply to the filtered lists that don't depend
241 | // on the index too!
242 |
243 | if (this._indexBound) {
244 |
245 | i = affectedIndex;
246 | node = this.get(i);
247 |
248 | // Iterate using the linked-list, it's faster than...
249 | // `for (i) { this.get(i); }`
250 | while (node) {
251 | node.data.index = i;
252 | can.batch.trigger(this, '__nodeModified', [node]);
253 | node = node.next;
254 | i++;
255 | }
256 | }
257 | },
258 |
259 | removeItems: function (items, offset) {
260 | var index = (items.length && items.length - 1) + offset;
261 |
262 | // Remove each item
263 | while (index >= offset) {
264 | this.removeItem(items[index], index);
265 | index--;
266 | }
267 | },
268 |
269 | removeItem: function (item, removedIndex) {
270 | this.unset(removedIndex, true);
271 | this._propagateIndexShift(removedIndex);
272 | },
273 |
274 | // Derived/filtered aren't writeable like traditional lists, they're
275 | // values are maintained via event bindings
276 | push: can.noop,
277 | pop: can.noop,
278 | shift: can.noop,
279 | unshift: can.noop,
280 | splice: can.noop,
281 | removeAttr: can.noop,
282 |
283 | _printIndexesValue: function (node) {
284 | return node.data.value;
285 | }
286 | });
287 |
288 | // Handle the adding/removing of items to the derived list based on
289 | // the predicate
290 | FilteredList = DerivedList.extend({
291 |
292 |
293 | init: function (sourceList, predicate, predicateContext) {
294 |
295 | this._includeComputes = [];
296 |
297 | // Overwrite the default predicate if one is provided
298 | if (predicate) {
299 | this.predicate = can.proxy(predicate, predicateContext || this);
300 | }
301 |
302 | // Mark this derived list as bound to indexes
303 | if (predicate.length > 1) {
304 | this._indexBound = true;
305 | }
306 |
307 | // Set the default comparator value normalize method to use
308 | // the source tree
309 | this._normalizeComparatorValue = this._getNodeIndexFromSource;
310 |
311 | // Setup bindings, initialize the tree (but don't populate the tree
312 | // with the items passed to the constructor)
313 | DerivedList.prototype.init.apply(this, [sourceList, true]);
314 | },
315 |
316 | syncValues: function () {
317 | this._source.bind('__nodeModified',
318 | can.proxy(this._evaluateIncludeComputeManually, this));
319 | },
320 |
321 | _evaluateIncludeComputeManually: function (ev, node) {
322 | var sourceData = node.data;
323 | var includeCompute = this._includeComputes[sourceData.index];
324 |
325 | if (! includeCompute) {
326 | return;
327 | }
328 |
329 | var oldValue = includeCompute.get();
330 | var newValue;
331 |
332 | includeCompute._on();
333 | newValue = includeCompute.get();
334 |
335 | if (newValue === oldValue) {
336 | return;
337 | }
338 |
339 | can.batch.trigger(includeCompute, {
340 | type: 'change',
341 | batchNum: can.batch.batchNum
342 | }, [
343 | newValue,
344 | oldValue
345 | ]);
346 | },
347 |
348 | syncRemoves: function () {
349 |
350 | var self = this;
351 |
352 | // Remove future items
353 | this._source.bind('pre-remove', function (ev, items, offset) {
354 | self.removeItems(items, offset);
355 | });
356 | },
357 |
358 |
359 | // Disable gaps in indexes
360 | _gapAndSize: function () {
361 | this.length++;
362 | },
363 |
364 | _comparator: function (_a, _b) {
365 | var a = this._normalizeComparatorValue(_a);
366 | var b = this._normalizeComparatorValue(_b);
367 | return a === b ? 0 : a < b ? -1 : 1; // ASC
368 | },
369 |
370 | _normalizeComparatorValue: function () {
371 | throw new Error(
372 | 'A method must be provided to normalize comparator values');
373 | },
374 |
375 | // Use a function that refers to the source tree when the comparator
376 | // is passed a node
377 | _getNodeIndexFromSource: function (value) {
378 | return value instanceof this.Node ?
379 | this._source.indexOfNode(value.data.node) :
380 | value;
381 | },
382 |
383 | // Use a function that refers to this tree when the comparator
384 | // is passed a node
385 | _getNodeIndexFromSelf: function (value) {
386 | return value instanceof this.Node ?
387 | this.indexOfNode(value) :
388 | value;
389 | },
390 |
391 | // By default, include all items
392 | predicate: function () { return true; },
393 |
394 | // Bind to index/value and determine whether to include/exclude the item
395 | // based on the predicate function provided by the user
396 | addItem: function (node) {
397 | var self = this;
398 | var nodeValue = node.data;
399 | var observedPredicate = new ObservedPredicate(this.predicate,
400 | this._source._source, nodeValue);
401 | var includeCompute = observedPredicate.includeCompute;
402 |
403 | // Add the item to the list of computes so that it can be
404 | // referenced and called later if the item is modified
405 | this._includeComputes.splice(nodeValue.index, 0, includeCompute);
406 |
407 | // Listen to changes on the predicate result
408 | includeCompute.bind('change', function (ev, newVal) {
409 | self._applyPredicateResult(nodeValue, newVal);
410 | });
411 |
412 | // Get the compute's cached value
413 | var res = includeCompute.get();
414 |
415 | // Apply the initial predicate result only if it's true
416 | // because there is a smidge of overhead involved in getting
417 | // the source index
418 | if (res) {
419 | this._applyPredicateResult(nodeValue, true);
420 | }
421 | },
422 |
423 | _applyPredicateResult: function (nodeValue, include) {
424 | var sourceIndex = this._source.indexOfNode(nodeValue.node);
425 |
426 | if (include) {
427 | this.set(sourceIndex, nodeValue, true);
428 | } else {
429 | this.unset(sourceIndex, true);
430 | }
431 | },
432 |
433 | removeItem: function (item, sourceIndex) {
434 | // Attempt to remove a node from the filtered tree
435 | // using the source tree's index
436 | this.unset(sourceIndex, true);
437 |
438 | this._includeComputes.splice(sourceIndex, 1);
439 | },
440 |
441 | // Iterate over the sourceItems' values instead of the node's data
442 | each: function (callback) {
443 | RBTreeList.prototype.eachNode.call(this, function (node, i) {
444 | return callback(node.data.value, i);
445 | });
446 | },
447 |
448 |
449 | ____get: function () {
450 |
451 | // Compare the passed index against the index of items in THIS tree
452 | this._normalizeComparatorValue = this._getNodeIndexFromSelf;
453 |
454 | var result = RBTreeList.prototype.____get.apply(this, arguments);
455 |
456 | // Revert back to the default behavior, which is to compare the passed
457 | // index against the index of items in the SOURCE tree
458 | this._normalizeComparatorValue = this._getNodeIndexFromSource;
459 |
460 | if (result && typeof result === 'object' && 'value' in result) {
461 | result = result.value;
462 | }
463 |
464 | return result;
465 | },
466 |
467 | // The default RBTreeList add/remove/pre-remove events pass the Node
468 | // as the newVal/oldVal, but the derived list is publicly consumed by
469 | // lots of things that think it's can.List-like; Instead dispatch the
470 | // event with the Node's "value" compute value
471 | _triggerChange: function (attr, how, newVal, oldVal) {
472 | var nodeConstructor = this.Node;
473 |
474 | // Modify existing newVal/oldVal arrays values
475 | can.each([newVal, oldVal], function (newOrOldValues) {
476 | can.each(newOrOldValues, function (value, index) {
477 | if (value instanceof nodeConstructor) {
478 | newOrOldValues[index] = value.data.value;
479 | }
480 | });
481 | });
482 |
483 | // Emit the event without any Node's as new/old values
484 | RBTreeList.prototype._triggerChange.apply(this, arguments);
485 | }
486 | });
487 |
488 | ObservedPredicate = function (predicate, sourceCollection, nodeValue) {
489 | this.predicate = predicate;
490 | this.nodeValue = nodeValue;
491 | this.sourceCollection = sourceCollection;
492 | this.includeCompute = new can.Compute(this.includeFn, this);
493 | };
494 |
495 | // Determine whether to include this item in the tree or not
496 | ObservedPredicate.prototype.includeFn = function () {
497 | var include, index, sourceCollection, value;
498 |
499 | index = this.nodeValue.index;
500 | value = this.nodeValue.value;
501 | sourceCollection = this.sourceCollection;
502 |
503 | // Enable sloppy map binds
504 | __observeAbstractValues = true;
505 |
506 | // Disregard bindings to the source item because the
507 | // source list's change event binding will handle this
508 | __observeException = value;
509 |
510 | // Point to our custom __observe definition that can be
511 | // configured to work differently
512 | can.__observe = __predicateObserve;
513 |
514 | // Use the predicate function to determine if this
515 | // item should be included in the overall list
516 | include = this.predicate(value, index, sourceCollection);
517 |
518 | // Turn off sloppy map binds
519 | __observeAbstractValues = false;
520 |
521 | // Remove the exception
522 | __observeException = undefined;
523 |
524 | // Revert to default can.__observe method
525 | can.__observe = __observe;
526 |
527 | return include;
528 | };
529 |
530 | // Add our unique filter method to the can.List prototype
531 | can.List.prototype.dFilter = DerivedList.prototype.dFilter;
532 |
533 | module.exports = DerivedList;
--------------------------------------------------------------------------------
/examples/filter/common.js:
--------------------------------------------------------------------------------
1 | window.NUMBER_OF_CIRCLES = 1000;
2 |
3 | window.numbersLib = {
4 | generateRandomNumber: function () {
5 | return Math.ceil(Math.random() * 99);
6 | },
7 | alternateNumber: window.alternateNumber = function (oldNum) {
8 | var isEven = oldNum % 2 === 0;
9 | var isOdd = ! isEven;
10 | var isLessThan = oldNum < 50;
11 | var isGreaterThan = ! isLessThan;
12 | var rand = Math.round(Math.random() * 24); // 0 - 24
13 |
14 | if (isEven && isLessThan) {
15 | num = rand * 2 + 1 + 50; // Odd (51 - 99)
16 | } else if (isEven && isGreaterThan) {
17 | num = rand * 2 + 1; // Even (1 - 49)
18 | } else if (isOdd && isLessThan) {
19 | num = rand * 2 + 50; // Even (50 - 98)
20 | } else if (isOdd && isGreaterThan) {
21 | num = rand * 2; // Even (0 - 48)
22 | }
23 |
24 | return num;
25 | }
26 | };
27 |
28 |
29 |
30 |
31 | /**
32 | * @author mrdoob / http://mrdoob.com/
33 | * @author jetienne / http://jetienne.com/
34 | * @author paulirish / http://paulirish.com/
35 | */
36 | var MemoryStats = function() {
37 |
38 | var msMin = 100;
39 | var msMax = 0;
40 |
41 | var container = document.createElement('div');
42 | container.id = 'stats';
43 | container.style.cssText = 'width:80px;opacity:0.9;cursor:pointer';
44 |
45 | var msDiv = document.createElement('div');
46 | msDiv.id = 'ms';
47 | msDiv.style.cssText = 'padding:0 0 3px 3px;text-align:left;background-color:#020;';
48 | container.appendChild(msDiv);
49 |
50 | var msText = document.createElement('div');
51 | msText.id = 'msText';
52 | msText.style.cssText = 'color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px';
53 | msText.innerHTML = 'Memory';
54 | msDiv.appendChild(msText);
55 |
56 | var msGraph = document.createElement('div');
57 | msGraph.id = 'msGraph';
58 | msGraph.style.cssText = 'position:relative;width:74px;height:30px;background-color:#0f0';
59 | msDiv.appendChild(msGraph);
60 |
61 | while (msGraph.children.length < 74) {
62 |
63 | var bar = document.createElement('span');
64 | bar.style.cssText = 'width:1px;height:30px;float:left;background-color:#131';
65 | msGraph.appendChild(bar);
66 |
67 | }
68 |
69 | var updateGraph = function(dom, height, color) {
70 |
71 | var child = dom.appendChild(dom.firstChild);
72 | child.style.height = height + 'px';
73 | if (color) child.style.backgroundColor = color;
74 |
75 | }
76 |
77 | var perf = window.performance || {};
78 | // polyfill usedJSHeapSize
79 | if (!perf && !perf.memory) {
80 | perf.memory = {
81 | usedJSHeapSize: 0
82 | };
83 | }
84 | if (perf && !perf.memory) {
85 | perf.memory = {
86 | usedJSHeapSize: 0
87 | };
88 | }
89 |
90 | // support of the API?
91 | if (perf.memory.totalJSHeapSize === 0) {
92 | console.warn('totalJSHeapSize === 0... performance.memory is only available in Chrome .')
93 | }
94 |
95 | // TODO, add a sanity check to see if values are bucketed.
96 | // If so, reminde user to adopt the --enable-precise-memory-info flag.
97 | // open -a "/Applications/Google Chrome.app" --args --enable-precise-memory-info
98 |
99 | var lastTime = Date.now();
100 | var lastUsedHeap = perf.memory.usedJSHeapSize;
101 | return {
102 | domElement: container,
103 |
104 | update: function() {
105 |
106 | // refresh only 30time per second
107 | if (Date.now() - lastTime < 1000 / 30) return;
108 | lastTime = Date.now()
109 |
110 | var delta = perf.memory.usedJSHeapSize - lastUsedHeap;
111 | lastUsedHeap = perf.memory.usedJSHeapSize;
112 | var color = delta < 0 ? '#830' : '#131';
113 |
114 | var ms = perf.memory.usedJSHeapSize;
115 | msMin = Math.min(msMin, ms);
116 | msMax = Math.max(msMax, ms);
117 | msText.textContent = "Mem: " + bytesToSize(ms, 2);
118 |
119 | var normValue = ms / (30 * 1024 * 1024);
120 | var height = Math.min(30, 30 - normValue * 30);
121 | updateGraph(msGraph, height, color);
122 |
123 | function bytesToSize(bytes, nFractDigit) {
124 | var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
125 | if (bytes == 0) return 'n/a';
126 | nFractDigit = nFractDigit !== undefined ? nFractDigit : 0;
127 | var precision = Math.pow(10, nFractDigit);
128 | var i = Math.floor(Math.log(bytes) / Math.log(1024));
129 | return Math.round(bytes * precision / Math.pow(1024, i)) / precision + ' ' + sizes[i];
130 | };
131 | }
132 |
133 | }
134 |
135 | };
136 |
137 |
138 |
139 |
140 |
141 | var Monitoring = Monitoring || (function() {
142 |
143 | var stats = new MemoryStats();
144 | stats.domElement.style.position = 'fixed';
145 | stats.domElement.style.right = '0px';
146 | stats.domElement.style.bottom = '0px';
147 | document.body.appendChild(stats.domElement);
148 | requestAnimationFrame(function rAFloop() {
149 | stats.update();
150 | requestAnimationFrame(rAFloop);
151 | });
152 |
153 | var RenderRate = function() {
154 | var container = document.createElement('div');
155 | container.id = 'stats';
156 | container.style.cssText = 'width:150px;opacity:0.9;cursor:pointer;position:fixed;right:80px;bottom:0px;';
157 |
158 | var msDiv = document.createElement('div');
159 | msDiv.id = 'ms';
160 | msDiv.style.cssText = 'padding:0 0 3px 3px;text-align:left;background-color:#020;';
161 | container.appendChild(msDiv);
162 |
163 | var msText = document.createElement('div');
164 | msText.id = 'msText';
165 | msText.style.cssText = 'color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px';
166 | msText.innerHTML = 'Repaint rate: 0/sec';
167 | msDiv.appendChild(msText);
168 |
169 | var bucketSize = 20;
170 | var bucket = [];
171 | var lastTime = Date.now();
172 | return {
173 | domElement: container,
174 | ping: function() {
175 | var start = lastTime;
176 | var stop = Date.now();
177 | var rate = 1000 / (stop - start);
178 | bucket.push(rate);
179 | if (bucket.length > bucketSize) {
180 | bucket.shift();
181 | }
182 | var sum = 0;
183 | for (var i = 0; i < bucket.length; i++) {
184 | sum = sum + bucket[i];
185 | }
186 | msText.textContent = "Repaint rate: " + (sum / bucket.length).toFixed(2) + "/sec";
187 | lastTime = stop;
188 | }
189 | }
190 | };
191 |
192 | var renderRate = new RenderRate();
193 | document.body.appendChild(renderRate.domElement);
194 |
195 | return {
196 | memoryStats: stats,
197 | renderRate: renderRate
198 | };
199 |
200 | })();
201 |
202 |
203 |
204 |
205 | // randomColor by David Merfield under the MIT license
206 | // https://github.com/davidmerfield/randomColor/
207 |
208 | ;
209 | (function(root, factory) {
210 |
211 | // Support AMD
212 | if (typeof define === 'function' && define.amd) {
213 | define([], factory);
214 |
215 | // Support CommonJS
216 | } else if (typeof exports === 'object') {
217 | var randomColor = factory();
218 |
219 | // Support NodeJS & Component, which allow module.exports to be a function
220 | if (typeof module === 'object' && module && module.exports) {
221 | exports = module.exports = randomColor;
222 | }
223 |
224 | // Support CommonJS 1.1.1 spec
225 | exports.randomColor = randomColor;
226 |
227 | // Support vanilla script loading
228 | } else {
229 | root.randomColor = factory();
230 | };
231 |
232 | }(this, function() {
233 |
234 | // Seed to get repeatable colors
235 | var seed = null;
236 |
237 | // Shared color dictionary
238 | var colorDictionary = {};
239 |
240 | // Populate the color dictionary
241 | loadColorBounds();
242 |
243 | var randomColor = function(options) {
244 | options = options || {};
245 | if (options.seed && !seed) seed = options.seed;
246 |
247 | var H, S, B;
248 |
249 | // Check if we need to generate multiple colors
250 | if (options.count != null) {
251 |
252 | var totalColors = options.count,
253 | colors = [];
254 |
255 | options.count = null;
256 |
257 | while (totalColors > colors.length) {
258 | colors.push(randomColor(options));
259 | }
260 |
261 | options.count = totalColors;
262 |
263 | //Keep the seed constant between runs.
264 | if (options.seed) seed = options.seed;
265 |
266 | return colors;
267 | }
268 |
269 | // First we pick a hue (H)
270 | H = pickHue(options);
271 |
272 | // Then use H to determine saturation (S)
273 | S = pickSaturation(H, options);
274 |
275 | // Then use S and H to determine brightness (B).
276 | B = pickBrightness(H, S, options);
277 |
278 | // Then we return the HSB color in the desired format
279 | return setFormat([H, S, B], options);
280 | };
281 |
282 | function pickHue(options) {
283 |
284 | var hueRange = getHueRange(options.hue),
285 | hue = randomWithin(hueRange);
286 |
287 | // Instead of storing red as two seperate ranges,
288 | // we group them, using negative numbers
289 | if (hue < 0) {
290 | hue = 360 + hue
291 | }
292 |
293 | return hue;
294 |
295 | }
296 |
297 | function pickSaturation(hue, options) {
298 |
299 | if (options.luminosity === 'random') {
300 | return randomWithin([0, 100]);
301 | }
302 |
303 | if (options.hue === 'monochrome') {
304 | return 0;
305 | }
306 |
307 | var saturationRange = getSaturationRange(hue);
308 |
309 | var sMin = saturationRange[0],
310 | sMax = saturationRange[1];
311 |
312 | switch (options.luminosity) {
313 |
314 | case 'bright':
315 | sMin = 55;
316 | break;
317 |
318 | case 'dark':
319 | sMin = sMax - 10;
320 | break;
321 |
322 | case 'light':
323 | sMax = 55;
324 | break;
325 | }
326 |
327 | return randomWithin([sMin, sMax]);
328 |
329 | }
330 |
331 | function pickBrightness(H, S, options) {
332 |
333 | var brightness,
334 | bMin = getMinimumBrightness(H, S),
335 | bMax = 100;
336 |
337 | switch (options.luminosity) {
338 |
339 | case 'dark':
340 | bMax = bMin + 20;
341 | break;
342 |
343 | case 'light':
344 | bMin = (bMax + bMin) / 2;
345 | break;
346 |
347 | case 'random':
348 | bMin = 0;
349 | bMax = 100;
350 | break;
351 | }
352 |
353 | return randomWithin([bMin, bMax]);
354 |
355 | }
356 |
357 | function setFormat(hsv, options) {
358 |
359 | switch (options.format) {
360 |
361 | case 'hsvArray':
362 | return hsv;
363 |
364 | case 'hslArray':
365 | return HSVtoHSL(hsv);
366 |
367 | case 'hsl':
368 | var hsl = HSVtoHSL(hsv);
369 | return 'hsl(' + hsl[0] + ', ' + hsl[1] + '%, ' + hsl[2] + '%)';
370 |
371 | case 'rgbArray':
372 | return HSVtoRGB(hsv);
373 |
374 | case 'rgb':
375 | var rgb = HSVtoRGB(hsv);
376 | return 'rgb(' + rgb.join(', ') + ')';
377 |
378 | default:
379 | return HSVtoHex(hsv);
380 | }
381 |
382 | }
383 |
384 | function getMinimumBrightness(H, S) {
385 |
386 | var lowerBounds = getColorInfo(H).lowerBounds;
387 |
388 | for (var i = 0; i < lowerBounds.length - 1; i++) {
389 |
390 | var s1 = lowerBounds[i][0],
391 | v1 = lowerBounds[i][1];
392 |
393 | var s2 = lowerBounds[i + 1][0],
394 | v2 = lowerBounds[i + 1][1];
395 |
396 | if (S >= s1 && S <= s2) {
397 |
398 | var m = (v2 - v1) / (s2 - s1),
399 | b = v1 - m * s1;
400 |
401 | return m * S + b;
402 | }
403 |
404 | }
405 |
406 | return 0;
407 | }
408 |
409 | function getHueRange(colorInput) {
410 |
411 | if (typeof parseInt(colorInput) === 'number') {
412 |
413 | var number = parseInt(colorInput);
414 |
415 | if (number < 360 && number > 0) {
416 | return [number, number];
417 | }
418 |
419 | }
420 |
421 | if (typeof colorInput === 'string') {
422 |
423 | if (colorDictionary[colorInput]) {
424 | var color = colorDictionary[colorInput];
425 | if (color.hueRange) {
426 | return color.hueRange
427 | }
428 | }
429 | }
430 |
431 | return [0, 360];
432 |
433 | }
434 |
435 | function getSaturationRange(hue) {
436 | return getColorInfo(hue).saturationRange;
437 | }
438 |
439 | function getColorInfo(hue) {
440 |
441 | // Maps red colors to make picking hue easier
442 | if (hue >= 334 && hue <= 360) {
443 | hue -= 360;
444 | }
445 |
446 | for (var colorName in colorDictionary) {
447 | var color = colorDictionary[colorName];
448 | if (color.hueRange &&
449 | hue >= color.hueRange[0] &&
450 | hue <= color.hueRange[1]) {
451 | return colorDictionary[colorName];
452 | }
453 | }
454 | return 'Color not found';
455 | }
456 |
457 | function randomWithin(range) {
458 | if (seed == null) {
459 | return Math.floor(range[0] + Math.random() * (range[1] + 1 - range[0]));
460 | } else {
461 | //Seeded random algorithm from http://indiegamr.com/generate-repeatable-random-numbers-in-js/
462 | var max = range[1] || 1;
463 | var min = range[0] || 0;
464 | seed = (seed * 9301 + 49297) % 233280;
465 | var rnd = seed / 233280.0;
466 | return Math.floor(min + rnd * (max - min));
467 | }
468 | }
469 |
470 | function HSVtoHex(hsv) {
471 |
472 | var rgb = HSVtoRGB(hsv);
473 |
474 | function componentToHex(c) {
475 | var hex = c.toString(16);
476 | return hex.length == 1 ? "0" + hex : hex;
477 | }
478 |
479 | var hex = "#" + componentToHex(rgb[0]) + componentToHex(rgb[1]) + componentToHex(rgb[2]);
480 |
481 | return hex;
482 |
483 | }
484 |
485 | function defineColor(name, hueRange, lowerBounds) {
486 |
487 | var sMin = lowerBounds[0][0],
488 | sMax = lowerBounds[lowerBounds.length - 1][0],
489 |
490 | bMin = lowerBounds[lowerBounds.length - 1][1],
491 | bMax = lowerBounds[0][1];
492 |
493 | colorDictionary[name] = {
494 | hueRange: hueRange,
495 | lowerBounds: lowerBounds,
496 | saturationRange: [sMin, sMax],
497 | brightnessRange: [bMin, bMax]
498 | };
499 |
500 | }
501 |
502 | function loadColorBounds() {
503 |
504 | defineColor(
505 | 'monochrome',
506 | null, [
507 | [0, 0],
508 | [100, 0]
509 | ]
510 | );
511 |
512 | defineColor(
513 | 'red', [-26, 18], [
514 | [20, 100],
515 | [30, 92],
516 | [40, 89],
517 | [50, 85],
518 | [60, 78],
519 | [70, 70],
520 | [80, 60],
521 | [90, 55],
522 | [100, 50]
523 | ]
524 | );
525 |
526 | defineColor(
527 | 'orange', [19, 46], [
528 | [20, 100],
529 | [30, 93],
530 | [40, 88],
531 | [50, 86],
532 | [60, 85],
533 | [70, 70],
534 | [100, 70]
535 | ]
536 | );
537 |
538 | defineColor(
539 | 'yellow', [47, 62], [
540 | [25, 100],
541 | [40, 94],
542 | [50, 89],
543 | [60, 86],
544 | [70, 84],
545 | [80, 82],
546 | [90, 80],
547 | [100, 75]
548 | ]
549 | );
550 |
551 | defineColor(
552 | 'green', [63, 178], [
553 | [30, 100],
554 | [40, 90],
555 | [50, 85],
556 | [60, 81],
557 | [70, 74],
558 | [80, 64],
559 | [90, 50],
560 | [100, 40]
561 | ]
562 | );
563 |
564 | defineColor(
565 | 'blue', [179, 257], [
566 | [20, 100],
567 | [30, 86],
568 | [40, 80],
569 | [50, 74],
570 | [60, 60],
571 | [70, 52],
572 | [80, 44],
573 | [90, 39],
574 | [100, 35]
575 | ]
576 | );
577 |
578 | defineColor(
579 | 'purple', [258, 282], [
580 | [20, 100],
581 | [30, 87],
582 | [40, 79],
583 | [50, 70],
584 | [60, 65],
585 | [70, 59],
586 | [80, 52],
587 | [90, 45],
588 | [100, 42]
589 | ]
590 | );
591 |
592 | defineColor(
593 | 'pink', [283, 334], [
594 | [20, 100],
595 | [30, 90],
596 | [40, 86],
597 | [60, 84],
598 | [80, 80],
599 | [90, 75],
600 | [100, 73]
601 | ]
602 | );
603 |
604 | }
605 |
606 | function HSVtoRGB(hsv) {
607 |
608 | // this doesn't work for the values of 0 and 360
609 | // here's the hacky fix
610 | var h = hsv[0];
611 | if (h === 0) {
612 | h = 1
613 | }
614 | if (h === 360) {
615 | h = 359
616 | }
617 |
618 | // Rebase the h,s,v values
619 | h = h / 360;
620 | var s = hsv[1] / 100,
621 | v = hsv[2] / 100;
622 |
623 | var h_i = Math.floor(h * 6),
624 | f = h * 6 - h_i,
625 | p = v * (1 - s),
626 | q = v * (1 - f * s),
627 | t = v * (1 - (1 - f) * s),
628 | r = 256,
629 | g = 256,
630 | b = 256;
631 |
632 | switch (h_i) {
633 | case 0:
634 | r = v, g = t, b = p;
635 | break;
636 | case 1:
637 | r = q, g = v, b = p;
638 | break;
639 | case 2:
640 | r = p, g = v, b = t;
641 | break;
642 | case 3:
643 | r = p, g = q, b = v;
644 | break;
645 | case 4:
646 | r = t, g = p, b = v;
647 | break;
648 | case 5:
649 | r = v, g = p, b = q;
650 | break;
651 | }
652 | var result = [Math.floor(r * 255), Math.floor(g * 255), Math.floor(b * 255)];
653 | return result;
654 | }
655 |
656 | function HSVtoHSL(hsv) {
657 | var h = hsv[0],
658 | s = hsv[1] / 100,
659 | v = hsv[2] / 100,
660 | k = (2 - s) * v;
661 |
662 | return [
663 | h,
664 | Math.round(s * v / (k < 1 ? k : 2 - k) * 10000) / 100,
665 | k / 2 * 100
666 | ];
667 | }
668 |
669 | return randomColor;
670 | }));
--------------------------------------------------------------------------------
/list/list_benchmark.js:
--------------------------------------------------------------------------------
1 | var Benchmark = require('benchmark');
2 | var can = require('can');
3 | var diff = require('can/util/array/diff.js');
4 |
5 | require("./list");
6 | require('can/view/autorender/autorender');
7 | require('can/view/stache/stache');
8 |
9 | var appState;
10 |
11 | /**
12 | * Utilites - Referenced by benchmarks
13 | **/
14 |
15 | var utils = {
16 | sandboxEl: document.getElementById('sandbox'),
17 | virtualDom: window.require("virtual-dom"),
18 | makeItem: function (index) {
19 | return {
20 | id: 'todo-' + index,
21 | completed: true
22 | };
23 | },
24 | makeArray: function (length) {
25 | var a = [];
26 | for (var i = 0; i < length; i++) {
27 | a.push(this.makeItem(i));
28 | }
29 | return a;
30 | },
31 | makePredicateFn: function () {
32 | return function (item) {
33 | return ((item.attr ? item.attr('completed') : item.completed) === true);
34 | };
35 | }
36 | };
37 |
38 | /**
39 | * Core - Benchmarks
40 | **/
41 |
42 | // Define default values
43 | var ResultMap = can.Map.extend({
44 | define: {
45 | runTest: {
46 | type: 'boolean',
47 | value: true
48 | },
49 | numberOfItems: {
50 | type: 'number'
51 | },
52 | '*': {
53 | value: '-'
54 | }
55 | }
56 | });
57 |
58 | var testResults = new can.List([
59 | new ResultMap({ runTest: true, numberOfItems: 1 }),
60 | new ResultMap({ runTest: true, numberOfItems: 10 }),
61 | new ResultMap({ runTest: true, numberOfItems: 100 }),
62 | new ResultMap({ runTest: true, numberOfItems: 1000 }),
63 | new ResultMap({ runTest: true, numberOfItems: 10 * 1000 }),
64 | new ResultMap({ runTest: true, numberOfItems: 100 * 1000 }),
65 | ]);
66 |
67 | var benchmarkSuite = new Benchmark.Suite('can.derive.List.dFilter')
68 | .on('cycle', function (ev) {
69 | var benchmark = ev.target;
70 | var averageMs = (benchmark.stats.mean * benchmark.adjustment) * 1000;
71 |
72 | console.log(benchmark.toString() +
73 | ' [Avg runtime: ' + averageMs + ']');
74 |
75 | benchmark.results.attr(benchmark.key, averageMs);
76 | });
77 |
78 | var setupBenchmarks = function () {
79 | testResults.each(function (results) {
80 |
81 | if (! results.attr('runTest')) {
82 | return;
83 | }
84 |
85 | if (appState.attr('options.runNativePopulate')) {
86 | benchmarkSuite.add(can.extend({
87 | adjustment: 1,
88 | results: results,
89 | key: 'nativePopulate',
90 | name: 'Native populate (' + results.numberOfItems + ' items)',
91 | setup: function () {
92 | /* jshint ignore:start */
93 |
94 | var numberOfItems = this.results.attr('numberOfItems');
95 | var source = this.makeArray(numberOfItems);
96 |
97 | /* jshint ignore:end */
98 | },
99 | fn: function () {
100 | /* jshint ignore:start */
101 |
102 | var filtered = source.filter(this.makePredicateFn());
103 |
104 | if (filtered.length !== numberOfItems) {
105 | throw 'Bad result';
106 | }
107 |
108 | /* jshint ignore:end */
109 | }
110 | }, utils));
111 |
112 | }
113 |
114 | if (appState.attr('options.runDerivePopulate')) {
115 | benchmarkSuite.add(can.extend({
116 | adjustment: 1,
117 | results: results,
118 | key: 'derivePopulate',
119 | name: 'Derived populate (' + results.numberOfItems + ' items)',
120 | setup: function () {
121 | /* jshint ignore:start */
122 |
123 | var numberOfItems = this.results.attr('numberOfItems');
124 | var values = this.makeArray(numberOfItems);
125 |
126 | var source = new can.List();
127 |
128 | values.forEach(function (element) {
129 | source.push(element);
130 | });
131 |
132 | /* jshint ignore:end */
133 | },
134 | fn: function () {
135 | /* jshint ignore:start */
136 |
137 | var filtered = source.dFilter(this.makePredicateFn());
138 |
139 | if (filtered.attr('length') !== numberOfItems) {
140 | throw 'Bad result';
141 | }
142 |
143 | // Unbind from the source list so that next
144 | // filter starts from scratch
145 | source._derivedList.unbindFromSource();
146 |
147 | /* jshint ignore:end */
148 | }
149 | }, utils));
150 | }
151 |
152 | if (appState.attr('options.runNativeUpdate')) {
153 | benchmarkSuite.add(can.extend({
154 | adjustment: 0.5,
155 | results: results,
156 | key: 'nativeUpdate',
157 | name: 'Native update (' + results.numberOfItems + ' items)',
158 | diff: diff,
159 | setup: function () {
160 | /* jshint ignore:start */
161 |
162 | var numberOfItems = this.results.attr('numberOfItems');
163 | var source = this.makeArray(numberOfItems);
164 | var updateIndex = numberOfItems - 1;
165 | var element = source[updateIndex];
166 |
167 | /* jshint ignore:end */
168 | },
169 | fn: function () {
170 | /* jshint ignore:start */
171 |
172 | // Update
173 | element.completed = ! element.completed;
174 | var filtered = source.filter(this.makePredicateFn());
175 |
176 | if (filtered.length !== numberOfItems - 1) {
177 | throw 'Bad result';
178 | }
179 |
180 | // Restore
181 | element.completed = ! element.completed;
182 | filtered = source.filter(this.makePredicateFn());
183 |
184 | if (filtered.length !== numberOfItems) {
185 | throw 'Bad result';
186 | }
187 |
188 | /* jshint ignore:end */
189 | }
190 | }, utils));
191 | }
192 |
193 | if (appState.attr('options.runDeriveUpdate')) {
194 | benchmarkSuite.add(can.extend({
195 | adjustment: 0.5,
196 | results: results,
197 | key: 'deriveUpdate',
198 | name: 'Derived update (' + results.numberOfItems + ' items)',
199 | setup: function () {
200 | /* jshint ignore:start */
201 |
202 | var numberOfItems = this.results.attr('numberOfItems');
203 | var values = this.makeArray(numberOfItems);
204 | var source = new can.List();
205 |
206 | values.forEach(function (element) {
207 | source.push(element);
208 | });
209 |
210 | var updateIndex = source.attr('length') - 1;
211 | var element = source.attr(updateIndex);
212 | var filtered = source.dFilter(this.makePredicateFn());
213 |
214 | /* jshint ignore:end */
215 | },
216 | fn: function () {
217 | /* jshint ignore:start */
218 |
219 | // Update
220 | element.attr('completed', ! element.attr('completed'));
221 |
222 | if (filtered.attr('length') !== numberOfItems - 1) {
223 | throw 'Bad result';
224 | }
225 |
226 | // Restore
227 | element.attr('completed', ! element.attr('completed'));
228 |
229 | if (filtered.attr('length') !== numberOfItems) {
230 | throw 'Bad result';
231 | }
232 |
233 | /* jshint ignore:end */
234 | }
235 | }, utils));
236 | }
237 |
238 | if (appState.attr('options.runVirtualDomUpdate') && results.numberOfItems <= 10 * 1000) {
239 | benchmarkSuite.add(can.extend({
240 | adjustment: 0.5,
241 | results: results,
242 | key: 'virtualDomUpdate',
243 | name: 'Virtual DOM update (' + results.numberOfItems + ' items)',
244 | render: function (filtered) {
245 | var h = this.virtualDom.h;
246 | return h('ul', {}, filtered.map(function (element) {
247 | return h("li", { key: element.id }, []);
248 | }));
249 | },
250 | updateDom: function (filtered) {
251 | /* jshint ignore:start */
252 |
253 | var newTree = this.render(filtered);
254 | var patches = this.virtualDom.diff(this.tree, newTree)
255 |
256 | this.virtualDom.patch(this.rootNode, patches);
257 | this.tree = newTree;
258 |
259 | /* jshint ignore:end */
260 | },
261 | setup: function () {
262 | /* jshint ignore:start */
263 |
264 | var numberOfItems = this.results.attr('numberOfItems');
265 | var source = this.makeArray(numberOfItems);
266 | var predicateFn = this.makePredicateFn();
267 | var filtered = source.filter(predicateFn);
268 | var updateIndex = numberOfItems - 1;
269 | var element = source[updateIndex];
270 |
271 | this.tree = this.render(filtered);
272 | this.rootNode = this.virtualDom.create(this.tree);
273 |
274 | $(this.sandboxEl).html(this.rootNode);
275 |
276 | /* jshint ignore:end */
277 | },
278 | fn: function () {
279 | /* jshint ignore:start */
280 |
281 | // Update
282 | element.completed = ! element.completed;
283 | var filtered = source.filter(predicateFn);
284 | this.updateDom(filtered);
285 |
286 | if (filtered.length !== numberOfItems - 1) {
287 | throw 'Bad result';
288 | }
289 |
290 | // Restore
291 | element.completed = ! element.completed;
292 | filtered = source.filter(predicateFn);
293 | this.updateDom(filtered);
294 |
295 | if (filtered.length !== numberOfItems) {
296 | throw 'Bad result';
297 | }
298 |
299 | /* jshint ignore:end */
300 | },
301 | teardown: function () {
302 | /* jshint ignore:start */
303 |
304 | $(this.sandboxEl).empty();
305 |
306 | /* jshint ignore:end */
307 | }
308 | }, utils));
309 | }
310 |
311 | if (appState.attr('options.runDeriveDomUpdate') && results.numberOfItems <= 10 * 1000) {
312 | benchmarkSuite.add(can.extend({
313 | adjustment: 0.5,
314 | results: results,
315 | key: 'deriveDomUpdate',
316 | name: 'Derived DOM update (' + results.numberOfItems + ' items)',
317 | // onCycle: function (ev) { debugger; },
318 | setup: function () {
319 | /* jshint ignore:start */
320 |
321 | var numberOfItems = this.results.attr('numberOfItems');
322 | var values = this.makeArray(numberOfItems);
323 | var source = new can.List();
324 |
325 | values.forEach(function (element) {
326 | source.push(element);
327 | });
328 |
329 | var updateIndex = source.attr('length') - 1;
330 | var element = source.attr(updateIndex);
331 | var filtered = source.dFilter(this.makePredicateFn());
332 |
333 | var renderer = can.stache(
334 | "