├── .gitignore
├── .jshintrc
├── .travis.yml
├── LICENSE.md
├── README.md
├── build.js
├── can-derive.js
├── examples
└── filter
│ ├── common.js
│ ├── perf-can-derive.html
│ ├── perf-can.html
│ ├── perf-react.html
│ ├── style.css
│ ├── test-drive.html
│ └── todomvc.html
├── list
├── benchmark.html
├── list.js
├── list_benchmark.js
├── list_test.js
└── test.html
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "globals": {
3 | "steal": true,
4 | "can": true,
5 | "QUnit": true,
6 | "test": true,
7 | "asyncTest": true,
8 | "expect": true,
9 | "module": true,
10 | "ok": true,
11 | "equal": true,
12 | "notEqual": true,
13 | "deepEqual": true,
14 | "notDeepEqual": true,
15 | "strictEqual": true,
16 | "notStrictEqual": true,
17 | "raises": true,
18 | "start": true,
19 | "stop": true
20 | },
21 |
22 |
23 | "curly": true,
24 | "eqeqeq": true,
25 | "freeze": true,
26 | "indent": 2,
27 | "latedef": true,
28 | "noarg": true,
29 | "undef": true,
30 | "unused": "vars",
31 | "trailing": true,
32 | "maxdepth": 4,
33 | "boss": true,
34 |
35 | "eqnull": true,
36 | "evil": true,
37 | "loopfunc": true,
38 | "smarttabs": true,
39 | "maxerr": 200,
40 |
41 | "jquery": true,
42 | "dojo": true,
43 | "mootools": true,
44 | "yui": true,
45 | "browser": true,
46 | "phantom": true,
47 | "rhino": true
48 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | node_js: 0.12
4 | script: npm test
5 | before_install:
6 | - "export DISPLAY=:99.0"
7 | - "sh -e /etc/init.d/xvfb start"
8 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Bitovi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/build.js:
--------------------------------------------------------------------------------
1 | var stealTools = require("steal-tools");
2 |
3 | stealTools.export({
4 | system: {
5 | config: __dirname + "/package.json!npm"
6 | },
7 | outputs: {
8 | "+amd": {},
9 | "+global-js": {},
10 | "+cjs": {}
11 | }
12 | }).catch(function(e){
13 | setTimeout(function(){
14 | throw e;
15 | }, 1);
16 | });
17 |
--------------------------------------------------------------------------------
/can-derive.js:
--------------------------------------------------------------------------------
1 | var List = require('list/list');
2 |
3 | var derivePlugin = {
4 | List: List
5 | };
6 |
7 | // Register the modified RBTreeList to the `can` namespace
8 | if (typeof window !== 'undefined' && !require.resolve && window.can) {
9 | window.can.derive = derivePlugin;
10 | }
11 |
12 | module.exports = derivePlugin;
--------------------------------------------------------------------------------
/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 | }));
--------------------------------------------------------------------------------
/examples/filter/perf-can-derive.html:
--------------------------------------------------------------------------------
1 |
.filter() perf - can-derive
2 |
3 |
4 |
5 |
6 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/examples/filter/perf-can.html:
--------------------------------------------------------------------------------
1 | .filter() perf - can-derive
2 |
3 |
4 |
5 |
6 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/examples/filter/perf-react.html:
--------------------------------------------------------------------------------
1 | .filter() perf - React
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
103 |
--------------------------------------------------------------------------------
/examples/filter/style.css:
--------------------------------------------------------------------------------
1 | html {
2 | font-family: sans-serif;
3 | font-size: 12px;
4 | }
5 | .cols {
6 | display: flex;
7 | flex-direction: column;
8 | }
9 |
10 | .col {
11 | flex: 1 0 auto;
12 | }
13 |
14 | .circle {
15 | /*display: none;*/
16 | float: left;
17 | width: 25px;
18 | height: 25px;
19 | /*margin: 0 2px 1px;*/
20 | text-align: center;
21 | line-height: 25px;
22 | /*border-radius: 50%;*/
23 | color: #fff;
24 | background-color: #eee;
25 | }
26 |
27 | #stats {
28 | zoom: 3;
29 | }
--------------------------------------------------------------------------------
/examples/filter/test-drive.html:
--------------------------------------------------------------------------------
1 | .filter() playground - can-derive
2 |
3 |
4 |
5 |
32 |
33 |
34 |
35 |
154 |
155 |
208 |
209 |
210 |
--------------------------------------------------------------------------------
/examples/filter/todomvc.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 |
15 |
16 |
17 |
18 |
37 |
38 |
39 |
59 |
63 |
75 |
76 |
77 |
78 |
79 |
167 |
168 |
169 |
--------------------------------------------------------------------------------
/list/benchmark.html:
--------------------------------------------------------------------------------
1 | can.List.prototype.dFilter Benchmarks
2 | can.List.prototype.dFilter Benchmarks
3 |
4 |
42 |
43 |
44 |
45 |
48 |
49 |
161 |
162 |
163 |
164 |
165 |
--------------------------------------------------------------------------------
/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;
--------------------------------------------------------------------------------
/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 | "\n{{#each filtered}}\n\n{{/each}}\n
");
335 |
336 | var fragment = renderer({
337 | filtered: filtered
338 | });
339 |
340 | $('#sandbox').html(fragment);
341 |
342 | /* jshint ignore:end */
343 | },
344 | fn: function () {
345 | /* jshint ignore:start */
346 |
347 | // Update
348 | element.attr('completed', ! element.attr('completed'));
349 |
350 | if (filtered.attr('length') !== numberOfItems - 1) {
351 | throw 'Bad result';
352 | }
353 |
354 | // Restore
355 | element.attr('completed', ! element.attr('completed'));
356 |
357 | if (filtered.attr('length') !== numberOfItems) {
358 | throw 'Bad result';
359 | }
360 |
361 | /* jshint ignore:end */
362 | },
363 | teardown: function () {
364 | /* jshint ignore:start */
365 |
366 | // Deal with async unbind inside of benchmarks for loop
367 | if (window.unbindComputes) { window.unbindComputes(); }
368 |
369 | // Remove the reference in the DOM that ties back to the
370 | // last filtered/source list
371 | $('#sandbox').empty();
372 |
373 | /* jshint ignore:end */
374 | }
375 | }, utils));
376 | }
377 |
378 |
379 | if (appState.attr('options.runReducedNativeUpdate')) {
380 | benchmarkSuite.add(can.extend({
381 | adjustment: 0.5,
382 | results: results,
383 | key: 'reducedNativeUpdate',
384 | name: 'Reduced Native update (' + results.numberOfItems + ' items)',
385 | diff: diff,
386 | setup: function () {
387 | /* jshint ignore:start */
388 |
389 | var numberOfItems = 100000;
390 | var filteredCount = this.results.attr('numberOfItems');
391 | var source = this.makeArray(numberOfItems);
392 |
393 | source.forEach(function (element, index) {
394 | if (index < filteredCount) {
395 | element.completed = true;
396 | } else {
397 | element.completed = false;
398 | }
399 | });
400 |
401 | var element = source[numberOfItems - 1];
402 |
403 | /* jshint ignore:end */
404 | },
405 | fn: function () {
406 | /* jshint ignore:start */
407 |
408 | // Update
409 | element.completed = ! element.completed;
410 | var filtered = source.filter(this.makePredicateFn());
411 |
412 | if (filtered.length !== filteredCount + (element.completed ? 1 : -1)) {
413 | throw 'Bad result';
414 | }
415 |
416 | // Restore
417 | element.completed = ! element.completed;
418 | filtered = source.filter(this.makePredicateFn());
419 |
420 | if (filtered.length !== filteredCount) {
421 | throw 'Bad result';
422 | }
423 |
424 | /* jshint ignore:end */
425 | }
426 | }, utils));
427 | }
428 |
429 | if (appState.attr('options.runReducedDeriveUpdate')) {
430 | benchmarkSuite.add(can.extend({
431 | adjustment: 0.5,
432 | results: results,
433 | key: 'reducedDeriveUpdate',
434 | name: 'Reduced Derived update (' + results.numberOfItems + ' items)',
435 | setup: function () {
436 | /* jshint ignore:start */
437 |
438 | var numberOfItems = 100000;
439 | var filteredCount = this.results.attr('numberOfItems');
440 | var values = this.makeArray(numberOfItems);
441 | var source = new can.List();
442 |
443 |
444 | values.forEach(function (element, index) {
445 | if (index < filteredCount) {
446 | element.completed = true;
447 | } else {
448 | element.completed = false;
449 | }
450 |
451 | source.push(element);
452 | });
453 |
454 | var element = source.attr(numberOfItems - 1);
455 | var filtered = source.dFilter(this.makePredicateFn());
456 |
457 | /* jshint ignore:end */
458 | },
459 | fn: function () {
460 | /* jshint ignore:start */
461 |
462 | // Update
463 | element.attr('completed', ! element.attr('completed'));
464 |
465 | if (filtered.attr('length') !== filteredCount + (element.attr('completed') ? 1 : -1)) {
466 | throw 'Bad result';
467 | }
468 |
469 | // Restore
470 | element.attr('completed', ! element.attr('completed'));
471 |
472 | if (filtered.attr('length') !== filteredCount) {
473 | throw 'Bad result';
474 | }
475 |
476 | /* jshint ignore:end */
477 | }
478 | }, utils));
479 | }
480 |
481 | if (appState.attr('options.runReducedNativeDomUpdate') && results.numberOfItems <= 10 * 1000) {
482 | benchmarkSuite.add(can.extend({
483 | adjustment: 0.5,
484 | results: results,
485 | key: 'reducedNativeDomUpdate',
486 | name: 'Reduced Native DOM update (' + results.numberOfItems + ' items)',
487 | diff: diff,
488 | render: function (filtered) {
489 | var h = this.virtualDom.h;
490 | return h('ul', {}, filtered.map(function (element) {
491 | return h("li", { key: element.id }, []);
492 | }));
493 | },
494 | updateDom: function (filtered) {
495 | /* jshint ignore:start */
496 |
497 | var newTree = this.render(filtered);
498 | var patches = this.virtualDom.diff(this.tree, newTree)
499 |
500 | this.virtualDom.patch(this.rootNode, patches);
501 | this.tree = newTree;
502 |
503 | /* jshint ignore:end */
504 | },
505 | setup: function () {
506 | /* jshint ignore:start */
507 |
508 | var numberOfItems = 10000;
509 | var filteredCount = this.results.attr('numberOfItems');
510 | var source = this.makeArray(numberOfItems);
511 |
512 | source.forEach(function (element, index) {
513 | if (index < filteredCount) {
514 | element.completed = true;
515 | } else {
516 | element.completed = false;
517 | }
518 | });
519 |
520 | var filtered = source.filter(this.makePredicateFn());
521 | var element = source[numberOfItems - 1];
522 |
523 |
524 | this.tree = this.render(filtered);
525 | this.rootNode = this.virtualDom.create(this.tree);
526 |
527 | $(this.sandboxEl).html(this.rootNode);
528 |
529 |
530 | /* jshint ignore:end */
531 | },
532 | fn: function () {
533 | /* jshint ignore:start */
534 |
535 | // Update
536 | element.completed = ! element.completed;
537 | filtered = source.filter(this.makePredicateFn());
538 | this.updateDom(filtered);
539 |
540 | if (filtered.length !== filteredCount + (element.completed ? 1 : -1)) {
541 | throw 'Bad result';
542 | }
543 |
544 | // Restore
545 | element.completed = ! element.completed;
546 | filtered = source.filter(this.makePredicateFn());
547 | this.updateDom(filtered);
548 |
549 | if (filtered.length !== filteredCount) {
550 | throw 'Bad result';
551 | }
552 |
553 | /* jshint ignore:end */
554 | },
555 | teardown: function () {
556 | /* jshint ignore:start */
557 |
558 | $(this.sandboxEl).empty();
559 |
560 | /* jshint ignore:end */
561 | }
562 | }, utils));
563 | }
564 |
565 | if (appState.attr('options.runReducedDeriveDomUpdate') && results.numberOfItems <= 10 * 1000) {
566 | benchmarkSuite.add(can.extend({
567 | adjustment: 0.5,
568 | results: results,
569 | key: 'reducedDeriveDomUpdate',
570 | name: 'Reduced Derived DOM update (' + results.numberOfItems + ' items)',
571 | setup: function () {
572 | /* jshint ignore:start */
573 |
574 | var numberOfItems = 10000;
575 | var filteredCount = this.results.attr('numberOfItems');
576 | var values = this.makeArray(numberOfItems);
577 | var source = new can.List();
578 |
579 |
580 | values.forEach(function (element, index) {
581 | if (index < filteredCount) {
582 | element.completed = true;
583 | } else {
584 | element.completed = false;
585 | }
586 |
587 | source.push(element);
588 | });
589 |
590 | var element = source.attr(numberOfItems - 1);
591 | var filtered = source.dFilter(this.makePredicateFn());
592 |
593 | var renderer = can.stache(
594 | "\n{{#each filtered}}\n\n{{/each}}\n
");
595 |
596 | var fragment = renderer({
597 | filtered: filtered
598 | });
599 |
600 | $(this.sandboxEl).html(fragment);
601 |
602 | /* jshint ignore:end */
603 | },
604 | fn: function () {
605 | /* jshint ignore:start */
606 |
607 | // Update
608 | element.attr('completed', ! element.attr('completed'));
609 |
610 | if (filtered.attr('length') !== filteredCount + (element.attr('completed') ? 1 : -1)) {
611 | throw 'Bad result';
612 | }
613 |
614 | // Restore
615 | element.attr('completed', ! element.attr('completed'));
616 |
617 | if (filtered.attr('length') !== filteredCount) {
618 | throw 'Bad result';
619 | }
620 |
621 | /* jshint ignore:end */
622 | },
623 | teardown: function () {
624 | /* jshint ignore:start */
625 |
626 | // Remove the reference in the DOM that ties back to the
627 | // last filtered/source list
628 | $(this.sandboxEl).empty();
629 |
630 | /* jshint ignore:end */
631 | }
632 | }, utils));
633 | }
634 |
635 | if (appState.attr('options.runNativeBatchUpdate')) {
636 | benchmarkSuite.add(can.extend({
637 | adjustment: 0.5,
638 | results: results,
639 | key: 'nativeBatchUpdate',
640 | name: 'Native batch update (' + results.numberOfItems + ' items)',
641 | diff: diff,
642 | setup: function () {
643 | /* jshint ignore:start */
644 |
645 | var numberOfItems = 100000;
646 | var batchCount = this.results.attr('numberOfItems');
647 | var source = this.makeArray(numberOfItems);
648 |
649 | /* jshint ignore:end */
650 | },
651 | fn: function () {
652 | /* jshint ignore:start */
653 |
654 | var element, i;
655 |
656 | // Update
657 | for (i = 0; i < batchCount; i++) {
658 | element = source[i];
659 | element.completed = ! element.completed;
660 | }
661 | filtered = source.filter(this.makePredicateFn());
662 |
663 | if (filtered.length !== numberOfItems - batchCount) {
664 | throw 'Bad result';
665 | }
666 |
667 | // Restore
668 | for (i = 0; i < batchCount; i++) {
669 | element = source[i];
670 | element.completed = ! element.completed;
671 | }
672 | filtered = source.filter(this.makePredicateFn());
673 |
674 | if (filtered.length !== numberOfItems) {
675 | throw 'Bad result';
676 | }
677 |
678 | /* jshint ignore:end */
679 | }
680 | }, utils));
681 | }
682 |
683 | if (appState.attr('options.runDeriveBatchUpdate')) {
684 | benchmarkSuite.add(can.extend({
685 | adjustment: 0.5,
686 | results: results,
687 | key: 'deriveBatchUpdate',
688 | name: 'Derived batch update (' + results.numberOfItems + ' items)',
689 | setup: function () {
690 | /* jshint ignore:start */
691 |
692 | var numberOfItems = 100000;
693 | var batchCount = this.results.attr('numberOfItems');
694 | var values = this.makeArray(numberOfItems);
695 | var source = new can.List();
696 |
697 | values.forEach(function (element, index) {
698 | source.push(element);
699 | });
700 |
701 | var filtered = source.dFilter(this.makePredicateFn());
702 |
703 | /* jshint ignore:end */
704 | },
705 | fn: function () {
706 | /* jshint ignore:start */
707 |
708 | var element, i;
709 |
710 | // Update
711 | for (i = 0; i < batchCount; i++) {
712 | element = source.attr(i);
713 | element.attr('completed', ! element.attr('completed'));
714 | }
715 |
716 | if (filtered.attr('length') !== numberOfItems - batchCount) {
717 | throw 'Bad result';
718 | }
719 |
720 | // Restore
721 | for (i = 0; i < batchCount; i++) {
722 | element = source.attr(i);
723 | element.attr('completed', ! element.attr('completed'));
724 | }
725 |
726 | if (filtered.attr('length') !== numberOfItems) {
727 | throw 'Bad result';
728 | }
729 |
730 | /* jshint ignore:end */
731 | }
732 | }, utils));
733 | }
734 | });
735 |
736 | /* jshint ignore:end */
737 | };
738 |
739 |
740 | /**
741 | * Options/Graphing UI
742 | **/
743 |
744 | can.Component.extend({
745 | tag: 'benchmark-options',
746 | template: can.view('benchmark-options-template'),
747 | viewModel: {
748 | define: {
749 | options: {
750 | value: {
751 | define: {
752 | '*': {
753 | type: 'boolean',
754 | value: false
755 | }
756 | }
757 | }
758 | },
759 | running: {
760 | value: false
761 | },
762 | testResults: {
763 | value: testResults
764 | }
765 | },
766 | init: function () {
767 | appState = this;
768 | can.route.map(this.attr('options'));
769 | can.route.ready();
770 |
771 | if (this.attr('options.startOnPageLoad')) {
772 | this.startBenchmarks();
773 | }
774 | },
775 | startBenchmarks: function () {
776 | var context = this;
777 |
778 | this.attr('running', true);
779 |
780 | setupBenchmarks();
781 |
782 | benchmarkSuite.on('complete', function () {
783 | context.attr('running', false);
784 | });
785 |
786 | // Render the button state before blocking repaints
787 | setTimeout(function () {
788 | benchmarkSuite.run({ async: false });
789 | }, 100);
790 | },
791 | resetOptions: function () {
792 | this.attr('options').attr({}, true);
793 | }
794 | }
795 | });
--------------------------------------------------------------------------------
/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/test.html:
--------------------------------------------------------------------------------
1 | can/list tests
2 |
3 |
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "can-derive",
3 | "version": "0.0.16",
4 | "description": "Derive a list/map from another via live binding",
5 | "main": "can-derive.js",
6 | "author": "Chris Gomez ",
7 | "license": "MIT",
8 | "keywords": [
9 | "canjs",
10 | "can",
11 | "observable",
12 | "bind"
13 | ],
14 | "repository": {
15 | "type": "git",
16 | "url": "git://github.com/canjs/can-derive.git"
17 | },
18 | "dependencies": {
19 | "can": "^2.3.0",
20 | "can-binarytree": "^0.0"
21 | },
22 | "devDependencies": {
23 | "benchmark": "1.0.0",
24 | "jshint": "2.7.0",
25 | "serve": "1.4.0",
26 | "steal": "0.13.0",
27 | "steal-qunit": "0.0.2",
28 | "steal-tools": "0.13.0",
29 | "system-npm": "0.3.0",
30 | "testee": "^0.2.0"
31 | },
32 | "system": {
33 | "meta": {
34 | "benchmark": {
35 | "format": "cjs"
36 | }
37 | }
38 | },
39 | "scripts": {
40 | "start": "serve -p 8080",
41 | "preversion": "npm test && npm run build",
42 | "version": "git commit -am \"Update dist for release\" && git checkout -b release && git add -f dist/",
43 | "postversion": "git push --tags && git checkout master && git branch -D release && git push",
44 | "release:patch": "npm version patch && npm publish",
45 | "release:minor": "npm version minor && npm publish",
46 | "release:major": "npm version major && npm publish",
47 | "jshint": "jshint list/. can-derive.js --config",
48 | "testee": "testee list/test.html --browsers phantom",
49 | "test": "npm run jshint && npm run testee",
50 | "build": "node build.js"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------