├── .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 | [![Build Status](https://travis-ci.org/canjs/can-derive.svg?branch=master)](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 | Fork me on GitHub 210 | -------------------------------------------------------------------------------- /examples/filter/todomvc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 14 | 15 | 16 | 17 | 18 | 37 | 38 | 39 |
40 | 41 | 47 |
48 |
    49 |
  • 50 |
    51 | 52 | 53 |
    54 |
  • 55 |
56 |
57 |
58 |
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 | ""); 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 | ""); 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 | --------------------------------------------------------------------------------