├── .vscode ├── launch.json └── tasks.json ├── README.md ├── bin ├── index.js ├── react-dom.js ├── react.js ├── require.js └── virtualized-scroll-viewer.js ├── index.html ├── libs ├── react-dom.js ├── react.js └── require.js ├── src ├── animated-group.ts ├── animated-size-group.ts ├── images.ts ├── index.tsx ├── tsconfig.json ├── typings │ └── react │ │ ├── react-addons-css-transition-group.d.ts │ │ ├── react-addons-transition-group.d.ts │ │ ├── react-dom.d.ts │ │ ├── react-global.d.ts │ │ └── react.d.ts ├── virtualized-list.tsx ├── virtualized-scroll-viewer-extensions.ts └── virtualized-scroll-viewer.ts ├── tsconfig.json └── tslint.json /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch index.html", 6 | "type": "chrome", 7 | "request": "launch", 8 | "file": "${workspaceRoot}/index.html", 9 | "sourceMaps": true, 10 | "userDataDir": "c:\\temp\\" 11 | }, 12 | { 13 | "name": "Launch localhost with sourcemaps", 14 | "type": "chrome", 15 | "request": "launch", 16 | "url": "http://localhost/mypage.html", 17 | "sourceMaps": true, 18 | "webRoot": "wwwroot", 19 | "userDataDir": "c:\\temp\\" 20 | }, 21 | { 22 | "name": "Attach", 23 | "type": "chrome", 24 | "request": "attach", 25 | "port": 9222 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | 9 | // A task runner that calls the Typescript compiler (tsc) and 10 | // Compiles a HelloWorld.ts program 11 | { 12 | "version": "0.1.0", 13 | 14 | // The command is tsc. Assumes that tsc has been installed using npm install -g typescript 15 | "command": "tsc", 16 | 17 | // The command is a shell script 18 | "isShellCommand": true, 19 | 20 | // Show the output window only if unrecognized errors occur. 21 | "showOutput": "silent", 22 | 23 | // args is the HelloWorld program to compile. 24 | "args": ["-p", "."], 25 | 26 | // use the standard tsc problem matcher to find compile problems 27 | // in the output. 28 | "problemMatcher": "$tsc" 29 | } 30 | 31 | // A task runner that calls the Typescript compiler (tsc) and 32 | // compiles based on a tsconfig.json file that is present in 33 | // the root of the folder open in VSCode 34 | /* 35 | { 36 | "version": "0.1.0", 37 | 38 | // The command is tsc. Assumes that tsc has been installed using npm install -g typescript 39 | "command": "tsc", 40 | 41 | // The command is a shell script 42 | "isShellCommand": true, 43 | 44 | // Show the output window only if unrecognized errors occur. 45 | "showOutput": "silent", 46 | 47 | // Tell the tsc compiler to use the tsconfig.json from the open folder. 48 | "args": ["-p", "."], 49 | 50 | // use the standard tsc problem matcher to find compile problems 51 | // in the output. 52 | "problemMatcher": "$tsc" 53 | } 54 | */ 55 | 56 | // A task runner configuration for gulp. Gulp provides a less task 57 | // which compiles less to css. 58 | /* 59 | { 60 | "version": "0.1.0", 61 | "command": "gulp", 62 | "isShellCommand": true, 63 | "tasks": [ 64 | { 65 | "taskName": "less", 66 | // Make this the default build command. 67 | "isBuildCommand": true, 68 | // Show the output window only if unrecognized errors occur. 69 | "showOutput": "silent", 70 | // Use the standard less compilation problem matcher. 71 | "problemMatcher": "$lessCompile" 72 | } 73 | ] 74 | } 75 | */ 76 | 77 | // Uncomment the following section to use jake to build a workspace 78 | // cloned from https://github.com/Microsoft/TypeScript.git 79 | /* 80 | { 81 | "version": "0.1.0", 82 | // Task runner is jake 83 | "command": "jake", 84 | // Need to be executed in shell / cmd 85 | "isShellCommand": true, 86 | "showOutput": "silent", 87 | "tasks": [ 88 | { 89 | // TS build command is local. 90 | "taskName": "local", 91 | // Make this the default build command. 92 | "isBuildCommand": true, 93 | // Show the output window only if unrecognized errors occur. 94 | "showOutput": "silent", 95 | // Use the redefined Typescript output problem matcher. 96 | "problemMatcher": [ 97 | "$tsc" 98 | ] 99 | } 100 | ] 101 | } 102 | */ 103 | 104 | // Uncomment the section below to use msbuild and generate problems 105 | // for csc, cpp, tsc and vb. The configuration assumes that msbuild 106 | // is available on the path and a solution file exists in the 107 | // workspace folder root. 108 | /* 109 | { 110 | "version": "0.1.0", 111 | "command": "msbuild", 112 | "args": [ 113 | // Ask msbuild to generate full paths for file names. 114 | "/property:GenerateFullPaths=true" 115 | ], 116 | "taskSelector": "/t:", 117 | "showOutput": "silent", 118 | "tasks": [ 119 | { 120 | "taskName": "build", 121 | // Show the output window only if unrecognized errors occur. 122 | "showOutput": "silent", 123 | // Use the standard MS compiler pattern to detect errors, warnings 124 | // and infos in the output. 125 | "problemMatcher": "$msCompile" 126 | } 127 | ] 128 | } 129 | */ 130 | 131 | // Uncomment the following section to use msbuild which compiles Typescript 132 | // and less files. 133 | /* 134 | { 135 | "version": "0.1.0", 136 | "command": "msbuild", 137 | "args": [ 138 | // Ask msbuild to generate full paths for file names. 139 | "/property:GenerateFullPaths=true" 140 | ], 141 | "taskSelector": "/t:", 142 | "showOutput": "silent", 143 | "tasks": [ 144 | { 145 | "taskName": "build", 146 | // Show the output window only if unrecognized errors occur. 147 | "showOutput": "silent", 148 | // Use the standard MS compiler pattern to detect errors, warnings 149 | // and infos in the output. 150 | "problemMatcher": [ 151 | "$msCompile", 152 | "$lessCompile" 153 | ] 154 | } 155 | ] 156 | } 157 | */ 158 | // A task runner example that defines a problemMatcher inline instead of using 159 | // a predefined one. 160 | /* 161 | { 162 | "version": "0.1.0", 163 | "command": "tsc", 164 | "isShellCommand": true, 165 | "args": ["HelloWorld.ts"], 166 | "showOutput": "silent", 167 | "problemMatcher": { 168 | // The problem is owned by the typescript language service. Ensure that the problems 169 | // are merged with problems produced by Visual Studio's language service. 170 | "owner": "typescript", 171 | // The file name for reported problems is relative to the current working directory. 172 | "fileLocation": ["relative", "${cwd}"], 173 | // The actual pattern to match problems in the output. 174 | "pattern": { 175 | // The regular expression. Matches HelloWorld.ts(2,10): error TS2339: Property 'logg' does not exist on type 'Console'. 176 | "regexp": "^([^\\s].*)\\((\\d+|\\d+,\\d+|\\d+,\\d+,\\d+,\\d+)\\):\\s+(error|warning|info)\\s+(TS\\d+)\\s*:\\s*(.*)$", 177 | // The match group that denotes the file containing the problem. 178 | "file": 1, 179 | // The match group that denotes the problem location. 180 | "location": 2, 181 | // The match group that denotes the problem's severity. Can be omitted. 182 | "severity": 3, 183 | // The match group that denotes the problem code. Can be omitted. 184 | "code": 4, 185 | // The match group that denotes the problem's message. 186 | "message": 5 187 | } 188 | } 189 | } 190 | */ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-virtualized-list 2 | Virtualization technique applied to a react list component 3 | 4 | Lists tend to grow and have hundreds of DOM elements which can become a problem specially on mobile devices. Using a virtualization technique (or windowing) we can discard the elements that are not visible on the viewport thus improving the performance when scrolling a list with thousands of elements. 5 | 6 | ### List features: 7 | - Items virtualization (only visible items are rendered) 8 | - Supports variable items' size (automatic size detection, no need to specify) 9 | - Supports items' animation 10 | - Smooth scrolling 11 | - Introduces extra elements but with minimal impact in css 12 | - Automatic feature detection: disables automatically when virtualization is not possible 13 | 14 | ### Animation features (CSSTransitionGroup like component included): 15 | - Supports transitions duration based on CSS (no need to set transition duration in code) 16 | - Supports transition from/to auto width/height 17 | 18 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | var __extends = (this && this.__extends) || function (d, b) { 2 | for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; 3 | function __() { this.constructor = d; } 4 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 5 | }; 6 | define("virtualized-scroll-viewer-extensions", ["require", "exports"], function (require, exports) { 7 | "use strict"; 8 | var ScrollExtensions; 9 | (function (ScrollExtensions) { 10 | var OVERFLOW_REGEX = /(auto|scroll)/; 11 | var NON_SCROLLABLE_ELEMENT_ATRIBUTE = "data-not-scrollable"; 12 | (function (ScrollDirection) { 13 | ScrollDirection[ScrollDirection["Horizontal"] = 0] = "Horizontal"; 14 | ScrollDirection[ScrollDirection["Vertical"] = 1] = "Vertical"; 15 | })(ScrollExtensions.ScrollDirection || (ScrollExtensions.ScrollDirection = {})); 16 | var ScrollDirection = ScrollExtensions.ScrollDirection; 17 | function getScrollHostInfo(element, excludeStaticParent) { 18 | if (element === null || element === undefined || element instanceof Document) { 19 | return { 20 | scrollHost: window, 21 | scrollDirection: ScrollDirection.Vertical, 22 | }; 23 | } 24 | var elementComputedStyle = getComputedStyle(element); 25 | excludeStaticParent = excludeStaticParent || elementComputedStyle.position === "absolute"; 26 | if (!excludeStaticParent || elementComputedStyle.position !== "static") { 27 | var isOverFlow = OVERFLOW_REGEX.test(elementComputedStyle.overflow + 28 | elementComputedStyle.overflowY + 29 | elementComputedStyle.overflowX); 30 | if (isOverFlow) { 31 | if (!element.hasAttribute(NON_SCROLLABLE_ELEMENT_ATRIBUTE)) { 32 | return { 33 | scrollHost: element, 34 | scrollDirection: OVERFLOW_REGEX.test(elementComputedStyle.overflowY) ? ScrollDirection.Vertical : 35 | ScrollDirection.Horizontal, 36 | }; 37 | } 38 | } 39 | } 40 | return getScrollHostInfo(element.parentElement, excludeStaticParent); 41 | } 42 | ScrollExtensions.getScrollHostInfo = getScrollHostInfo; 43 | function getScrollInfo(scrollHost) { 44 | if (scrollHost instanceof Window) { 45 | return { 46 | scrollHost: scrollHost, 47 | scroll: { 48 | x: document.body.scrollLeft, 49 | y: document.body.scrollTop, 50 | height: document.body.scrollHeight, 51 | width: document.body.scrollWidth, 52 | }, 53 | viewport: { 54 | x: 0, 55 | y: 0, 56 | height: scrollHost.innerHeight, 57 | width: scrollHost.innerWidth, 58 | }, 59 | }; 60 | } 61 | else if (scrollHost instanceof HTMLElement) { 62 | return { 63 | scrollHost: scrollHost, 64 | scroll: { 65 | x: scrollHost.scrollLeft, 66 | y: scrollHost.scrollTop, 67 | height: scrollHost.scrollHeight, 68 | width: scrollHost.scrollWidth, 69 | }, 70 | viewport: { 71 | x: 0, 72 | y: 0, 73 | height: scrollHost.clientHeight, 74 | width: scrollHost.clientWidth, 75 | }, 76 | }; 77 | } 78 | return null; 79 | } 80 | ScrollExtensions.getScrollInfo = getScrollInfo; 81 | function setScrollOffset(scrollHost, x, y, increment) { 82 | if (increment === void 0) { increment = false; } 83 | if (scrollHost instanceof Window) { 84 | scrollHost = document.body; 85 | } 86 | var scrollHostElement = scrollHost; 87 | if (!isNaN(y)) { 88 | if (increment) { 89 | y += scrollHostElement.scrollTop; 90 | } 91 | scrollHostElement.scrollTop = y; 92 | } 93 | if (!isNaN(x)) { 94 | if (increment) { 95 | x += scrollHostElement.scrollLeft; 96 | } 97 | scrollHostElement.scrollLeft = x; 98 | } 99 | } 100 | ScrollExtensions.setScrollOffset = setScrollOffset; 101 | })(ScrollExtensions = exports.ScrollExtensions || (exports.ScrollExtensions = {})); 102 | var ObjectExtensions; 103 | (function (ObjectExtensions) { 104 | function assign(target) { 105 | var sources = []; 106 | for (var _i = 1; _i < arguments.length; _i++) { 107 | sources[_i - 1] = arguments[_i]; 108 | } 109 | if (target == null) { 110 | throw new TypeError("Cannot convert undefined or null to object"); 111 | } 112 | target = Object(target); 113 | for (var index = 1; index < arguments.length; index++) { 114 | var source = arguments[index]; 115 | if (source != null) { 116 | for (var key in source) { 117 | if (Object.prototype.hasOwnProperty.call(source, key)) { 118 | target[key] = source[key]; 119 | } 120 | } 121 | } 122 | } 123 | return target; 124 | } 125 | ObjectExtensions.assign = assign; 126 | })(ObjectExtensions = exports.ObjectExtensions || (exports.ObjectExtensions = {})); 127 | }); 128 | define("animated-group", ["require", "exports", "react", "react-dom", "virtualized-scroll-viewer-extensions"], function (require, exports, React, ReactDOM, virtualized_scroll_viewer_extensions_1) { 129 | "use strict"; 130 | var ANIMATION_APPEAR = "-appear"; 131 | var ANIMATION_ENTER = "-enter"; 132 | var ANIMATION_LEAVE = "-leave"; 133 | var ANIMATION_ACTIVE = "-active"; 134 | var TICK = 17; 135 | var AnimatedGroup = (function (_super) { 136 | __extends(AnimatedGroup, _super); 137 | function AnimatedGroup() { 138 | _super.apply(this, arguments); 139 | } 140 | AnimatedGroup.prototype.getDefaultTransitionName = function () { 141 | return ""; 142 | }; 143 | AnimatedGroup.prototype.getAnimatedItem = function () { 144 | return AnimatedItem; 145 | }; 146 | AnimatedGroup.prototype.wrapChild = function (child) { 147 | var childAttributes = { 148 | shouldSuspendAnimations: this.props.shouldSuspendAnimations, 149 | transitionName: this.props.transitionName || this.getDefaultTransitionName(), 150 | onEnter: this.props.onEnter, 151 | onEnterStarted: this.props.onEnterStarted, 152 | onLeave: this.props.onLeave, 153 | onLeaveStarted: this.props.onLeaveStarted 154 | }; 155 | return React.createElement(this.getAnimatedItem(), virtualized_scroll_viewer_extensions_1.ObjectExtensions.assign({}, child.props, childAttributes), child); 156 | }; 157 | AnimatedGroup.prototype.render = function () { 158 | return React.createElement(React.addons.TransitionGroup, virtualized_scroll_viewer_extensions_1.ObjectExtensions.assign({}, this.props, { childFactory: this.wrapChild.bind(this) }), this.props.children); 159 | }; 160 | return AnimatedGroup; 161 | }(React.Component)); 162 | exports.AnimatedGroup = AnimatedGroup; 163 | var AnimatedItem = (function (_super) { 164 | __extends(AnimatedItem, _super); 165 | function AnimatedItem() { 166 | _super.apply(this, arguments); 167 | this.transitionTimeouts = []; 168 | } 169 | AnimatedItem.prototype.getAnimationClassName = function (element) { 170 | return this.props.transitionName; 171 | }; 172 | AnimatedItem.prototype.queueAction = function (action, timeout) { 173 | var timeoutHandle = setTimeout(action, timeout); 174 | this.transitionTimeouts.push(timeoutHandle); 175 | }; 176 | AnimatedItem.prototype.transition = function (transitionName, done, onStart, onStartTransition, onEnd) { 177 | var _this = this; 178 | if (this.props.shouldSuspendAnimations && this.props.shouldSuspendAnimations()) { 179 | done(); 180 | return; 181 | } 182 | var element = ReactDOM.findDOMNode(this); 183 | var animationClassName = this.getAnimationClassName(element) + transitionName; 184 | onStart(element); 185 | element.classList.add(animationClassName); 186 | this.queueAction(function () { 187 | element.classList.add(animationClassName + ANIMATION_ACTIVE); 188 | var elementStyle = getComputedStyle(element); 189 | var animationDuration = parseFloat(elementStyle.transitionDelay) + parseFloat(elementStyle.transitionDuration); 190 | onStartTransition(element); 191 | var animationEnd = function () { 192 | element.classList.remove(animationClassName); 193 | element.classList.remove(animationClassName + ANIMATION_ACTIVE); 194 | onEnd(element); 195 | done(); 196 | }; 197 | _this.queueAction(animationEnd, animationDuration * 1000); 198 | }, TICK); 199 | }; 200 | AnimatedItem.prototype.componentWillAppear = function (done) { 201 | var _this = this; 202 | this.transition(ANIMATION_APPEAR, done, function (element) { return _this.startEnter(element); }, function (element) { return _this.startEnterTransition(element); }, function (element) { return _this.endEnter(element); }); 203 | }; 204 | AnimatedItem.prototype.componentWillEnter = function (done) { 205 | var _this = this; 206 | this.transition(ANIMATION_ENTER, done, function (element) { return _this.startEnter(element); }, function (element) { return _this.startEnterTransition(element); }, function (element) { return _this.endEnter(element); }); 207 | }; 208 | AnimatedItem.prototype.startEnter = function (element) { }; 209 | AnimatedItem.prototype.startEnterTransition = function (element) { 210 | if (this.props.onEnterStarted) { 211 | this.props.onEnterStarted(); 212 | } 213 | }; 214 | AnimatedItem.prototype.endEnter = function (element) { }; 215 | AnimatedItem.prototype.componentWillLeave = function (done) { 216 | var _this = this; 217 | this.transition(ANIMATION_LEAVE, done, function (element) { return _this.startLeave(element); }, function (element) { return _this.startLeaveTransition(element); }, function (element) { return _this.endLeave(element); }); 218 | }; 219 | AnimatedItem.prototype.startLeave = function (element) { }; 220 | AnimatedItem.prototype.startLeaveTransition = function (element) { 221 | if (this.props.onLeaveStarted) { 222 | this.props.onLeaveStarted(); 223 | } 224 | }; 225 | AnimatedItem.prototype.endLeave = function (element) { }; 226 | AnimatedItem.prototype.componentWillUnmount = function () { 227 | this.transitionTimeouts.forEach(function (t) { return clearTimeout(t); }); 228 | this.transitionTimeouts = []; 229 | }; 230 | AnimatedItem.prototype.componentDidAppear = function () { 231 | if (this.props.onEnter) { 232 | this.props.onEnter(); 233 | } 234 | }; 235 | AnimatedItem.prototype.componentDidEnter = function () { 236 | if (this.props.onEnter) { 237 | this.props.onEnter(); 238 | } 239 | }; 240 | AnimatedItem.prototype.componentDidLeave = function () { 241 | if (this.props.onLeave) { 242 | this.props.onLeave(); 243 | } 244 | }; 245 | AnimatedItem.prototype.render = function () { 246 | return React.Children.only(this.props.children); 247 | }; 248 | return AnimatedItem; 249 | }(React.Component)); 250 | exports.AnimatedItem = AnimatedItem; 251 | }); 252 | define("animated-size-group", ["require", "exports", "animated-group"], function (require, exports, animated_group_1) { 253 | "use strict"; 254 | var PIXELS_UNIT = "px"; 255 | var AnimatedSizeGroup = (function (_super) { 256 | __extends(AnimatedSizeGroup, _super); 257 | function AnimatedSizeGroup() { 258 | _super.apply(this, arguments); 259 | } 260 | AnimatedSizeGroup.prototype.getAnimatedItem = function () { 261 | return AnimatedSizeItem; 262 | }; 263 | return AnimatedSizeGroup; 264 | }(animated_group_1.AnimatedGroup)); 265 | exports.AnimatedSizeGroup = AnimatedSizeGroup; 266 | var AnimatedSizeItem = (function (_super) { 267 | __extends(AnimatedSizeItem, _super); 268 | function AnimatedSizeItem() { 269 | _super.apply(this, arguments); 270 | } 271 | AnimatedSizeItem.prototype.isDisplayInline = function (style) { 272 | return style && style.display.indexOf("inline") === 0; 273 | }; 274 | AnimatedSizeItem.prototype.getAnimationClassName = function (element) { 275 | var animationClassName = _super.prototype.getAnimationClassName.call(this, element); 276 | var style = getComputedStyle(element); 277 | if (this.isDisplayInline(style)) { 278 | animationClassName += "-inline"; 279 | } 280 | return animationClassName; 281 | }; 282 | AnimatedSizeItem.prototype.storeStyleSize = function (element) { 283 | this.previousStyleWidth = element.style.width; 284 | this.previousStyleHeight = element.style.height; 285 | }; 286 | AnimatedSizeItem.prototype.restorePreviousStyleSize = function (element) { 287 | element.style.width = this.previousStyleWidth; 288 | element.style.height = this.previousStyleHeight; 289 | }; 290 | AnimatedSizeItem.prototype.startEnter = function (element) { 291 | _super.prototype.startEnter.call(this, element); 292 | var elementBounds = element.getBoundingClientRect(); 293 | this.previousWidth = elementBounds.width; 294 | this.previousHeight = elementBounds.height; 295 | }; 296 | AnimatedSizeItem.prototype.startEnterTransition = function (element) { 297 | _super.prototype.startEnterTransition.call(this, element); 298 | this.storeStyleSize(element); 299 | var elementBounds = element.getBoundingClientRect(); 300 | if (elementBounds.width !== this.previousWidth) { 301 | element.style.width = this.previousWidth + PIXELS_UNIT; 302 | } 303 | if (elementBounds.height !== this.previousHeight) { 304 | element.style.height = this.previousHeight + PIXELS_UNIT; 305 | } 306 | }; 307 | AnimatedSizeItem.prototype.endEnter = function (element) { 308 | _super.prototype.endEnter.call(this, element); 309 | this.restorePreviousStyleSize(element); 310 | }; 311 | AnimatedSizeItem.prototype.startLeave = function (element) { 312 | _super.prototype.startLeave.call(this, element); 313 | this.storeStyleSize(element); 314 | var elementBounds = element.getBoundingClientRect(); 315 | element.style.width = elementBounds.width + PIXELS_UNIT; 316 | element.style.height = elementBounds.height + PIXELS_UNIT; 317 | }; 318 | AnimatedSizeItem.prototype.startLeaveTransition = function (element) { 319 | _super.prototype.startLeaveTransition.call(this, element); 320 | this.restorePreviousStyleSize(element); 321 | }; 322 | return AnimatedSizeItem; 323 | }(animated_group_1.AnimatedItem)); 324 | exports.AnimatedSizeItem = AnimatedSizeItem; 325 | }); 326 | define("images", ["require", "exports"], function (require, exports) { 327 | "use strict"; 328 | exports.Images = [ 329 | "http://24.media.tumblr.com/tumblr_m3sdvsBdJU1qzzim1o1_500.jpg", 330 | "http://25.media.tumblr.com/tumblr_m1zlwpDjAh1qze0hyo1_1280.jpg", 331 | "http://25.media.tumblr.com/tumblr_m1wlprhmJY1qjmz65o1_500.gif", 332 | "http://25.media.tumblr.com/tumblr_lkvj1aVSQg1qbe5pxo1_1280.jpg", 333 | "http://24.media.tumblr.com/Jjkybd3nSk6rkrg7VU07HTxGo1_500.jpg", 334 | "http://29.media.tumblr.com/tumblr_lr64gw71Ag1qjqdfao1_500.png", 335 | "http://25.media.tumblr.com/tumblr_m3jfavG5eN1r73wdao1_500.jpg", 336 | "http://24.media.tumblr.com/tumblr_m35g24e4z11qzex9io1_1280.jpg", 337 | "http://24.media.tumblr.com/tumblr_m1nmoduuK21qzex9io1_1280.jpg", 338 | "http://24.media.tumblr.com/YyXwbMbaOoa3u0trL7uZedmPo1_400.gif", 339 | "http://25.media.tumblr.com/tumblr_lh0dypa6W21qgnva2o1_500.jpg", 340 | "http://24.media.tumblr.com/tumblr_m2o12tB0pn1qze0hyo1_1280.jpg", 341 | "http://25.media.tumblr.com/tumblr_kp3d65xe3l1qz9bf5o1_500.jpg", 342 | "http://24.media.tumblr.com/tumblr_lycrqshTFf1r38hk2o1_400.jpg", 343 | "http://26.media.tumblr.com/tumblr_ly9qoi8doW1qbt33io1_500.jpg", 344 | "http://24.media.tumblr.com/tumblr_m1um2sieES1qzex9io1_1280.jpg", 345 | "http://26.media.tumblr.com/tumblr_ltwqocDfjo1qdvbl3o1_1280.jpg", 346 | "http://29.media.tumblr.com/tumblr_m0r6hmzVYx1qbhms5o1_500.jpg", 347 | "http://30.media.tumblr.com/tumblr_m1pggypRMz1qjahcpo1_500.jpg", 348 | "http://25.media.tumblr.com/tumblr_m2u72wnvk11qhwmnpo1_1280.jpg", 349 | "http://25.media.tumblr.com/tumblr_lrcyxlchQZ1qfpssto1_r1_500.gif", 350 | "http://25.media.tumblr.com/tumblr_ln2uaqQ5Rz1qdvbl3o1_1280.jpg", 351 | "http://29.media.tumblr.com/tumblr_lyvsgqn1H51qbvr04o1_500.gif", 352 | "http://24.media.tumblr.com/tumblr_ly2lzj9yzP1qk4s2co1_1280.jpg", 353 | "http://24.media.tumblr.com/tumblr_lz032duhSH1qa10uwo1_1280.png", 354 | "http://24.media.tumblr.com/tumblr_m1fvvw2cS71qz5dg8o1_1280.jpg", 355 | "http://25.media.tumblr.com/tumblr_lz920fsYPB1rowplqo1_1280.jpg", 356 | "http://27.media.tumblr.com/tumblr_m0xhjbRFCp1r6b7kmo1_500.gif", 357 | "http://24.media.tumblr.com/tumblr_m2p47tjFdg1qa7xfro1_1280.jpg", 358 | "http://25.media.tumblr.com/tumblr_m4ilt9rDjh1r6jd7fo1_1280.jpg", 359 | "http://25.media.tumblr.com/tumblr_m3fdtouawb1qhwmnpo1_1280.jpg", 360 | "http://24.media.tumblr.com/tumblr_m0iuyhHhwD1qbe5pxo1_500.jpg", 361 | "http://26.media.tumblr.com/tumblr_ly17xkVXnc1qbt33io1_1280.jpg", 362 | "http://25.media.tumblr.com/tumblr_m8im7ptqs81rdogh7o1_1280.jpg", 363 | "http://27.media.tumblr.com/tumblr_m1pgyeuGDs1qjahcpo1_250.jpg", 364 | "http://24.media.tumblr.com/tumblr_lvbpb9op931qc1sduo1_500.jpg", 365 | "http://26.media.tumblr.com/tumblr_lhzo4g1ZiG1qfyzelo1_500.jpg", 366 | "http://24.media.tumblr.com/tumblr_luowrcYoTj1qdth8zo1_1280.jpg", 367 | "http://25.media.tumblr.com/tumblr_lee9o3ssrv1qcn249o1_400.gif", 368 | "http://24.media.tumblr.com/Jjkybd3nSbuwu42gf8MzkTik_500.jpg", 369 | "http://25.media.tumblr.com/tumblr_m4ijv28wlO1r6jd7fo1_500.jpg", 370 | "http://26.media.tumblr.com/tumblr_m20fbkANHa1qzex9io1_1280.jpg", 371 | "http://24.media.tumblr.com/tumblr_m0uo7n1nA01qzv52ko1_1280.jpg", 372 | "http://25.media.tumblr.com/tumblr_loxwm8v8fH1qjmniro1_400.gif", 373 | "http://25.media.tumblr.com/tumblr_m0oki3v6RC1qi9p54o1_500.jpg", 374 | "http://25.media.tumblr.com/tumblr_m7gbozreKr1qzex9io1_1280.jpg", 375 | "http://29.media.tumblr.com/tumblr_lrftlutgUt1qgn992o1_500.jpg", 376 | "http://25.media.tumblr.com/tumblr_m109jwrekN1qjc1a7o1_500.jpg", 377 | "http://24.media.tumblr.com/tumblr_m4mi59MLm91qz5l2xo1_500.png", 378 | "http://24.media.tumblr.com/tumblr_m4r5g38v4z1qh66wqo1_1280.jpg" 379 | ]; 380 | }); 381 | define("virtualized-scroll-viewer", ["require", "exports", "react", "react-dom", "virtualized-scroll-viewer-extensions"], function (require, exports, React, ReactDOM, virtualized_scroll_viewer_extensions_2) { 382 | "use strict"; 383 | function insideiOSWebView() { 384 | return !navigator.standalone && /(iPad)|(iPhone)/i.test(navigator.userAgent) && !/safari/i.test(navigator.userAgent); 385 | } 386 | var SCROLL_EVENT_NAME = "scroll"; 387 | var RESIZE_EVENT_NAME = "resize"; 388 | var PIXEL_UNITS = "px"; 389 | var FLEXBOX_DISPLAY = document.createElement("p").style.flex === undefined ? "-webkit-flex" : "flex"; 390 | var DEFAULT_BUFFER_SIZE = 3; 391 | var BUFFER_MULTIPLIER = insideiOSWebView() ? 4 : 1; 392 | var MIN_ITEM_SIZE = 20; 393 | var VirtualizedScrollViewer = (function (_super) { 394 | __extends(VirtualizedScrollViewer, _super); 395 | function VirtualizedScrollViewer(props, context) { 396 | _super.call(this, props, context); 397 | this.scrollDirection = virtualized_scroll_viewer_extensions_2.ScrollExtensions.ScrollDirection.Vertical; 398 | this.hasPendingPropertiesUpdate = false; 399 | this.isScrollOngoing = false; 400 | this.isComponentInitialized = false; 401 | this.scrollHandler = this.handleScroll.bind(this); 402 | this.state = { 403 | firstRenderedItemIndex: 0, 404 | lastRenderedItemIndex: 1, 405 | averageItemSize: 0, 406 | scrollOffset: 0, 407 | offScreenItemsCount: 0, 408 | effectiveScrollOffset: Number.MIN_VALUE, 409 | }; 410 | } 411 | VirtualizedScrollViewer.prototype.getScrollHostInfo = function () { 412 | if (!this.scrollHostInfo) { 413 | this.scrollHostInfo = virtualized_scroll_viewer_extensions_2.ScrollExtensions.getScrollHostInfo(this.itemsContainer); 414 | } 415 | return this.scrollHostInfo; 416 | }; 417 | VirtualizedScrollViewer.prototype.getScrollInfo = function () { 418 | var scrollHostInfo = this.getScrollHostInfo(); 419 | var scrollHost = scrollHostInfo.scrollHost; 420 | var scrollInfo = virtualized_scroll_viewer_extensions_2.ScrollExtensions.getScrollInfo(scrollHost); 421 | var result = { 422 | scrollHost: scrollHost, 423 | scrollOffset: this.getDimension(scrollInfo.scroll.y, scrollInfo.scroll.x), 424 | viewportSize: this.getDimension(scrollInfo.viewport.height, scrollInfo.viewport.width), 425 | viewportLowerBound: 0, 426 | viewportUpperBound: 0, 427 | }; 428 | if (scrollHost instanceof Window) { 429 | result.viewportLowerBound = this.getDimension(scrollInfo.viewport.y, scrollInfo.viewport.x); 430 | result.viewportUpperBound = this.getDimension(scrollInfo.viewport.height, scrollInfo.viewport.width); 431 | } 432 | else if (scrollHost instanceof HTMLElement) { 433 | var bounds = scrollHost.getBoundingClientRect(); 434 | result.viewportLowerBound = this.getDimension(bounds.top, bounds.left); 435 | result.viewportUpperBound = this.getDimension(bounds.bottom, bounds.right); 436 | } 437 | return result; 438 | }; 439 | VirtualizedScrollViewer.prototype.addScrollHandler = function () { 440 | if (this.isDisposed) { 441 | return; 442 | } 443 | this.scrollHostInfo = null; 444 | var scrollHostInfo = this.getScrollHostInfo(); 445 | var scrollHost = scrollHostInfo.scrollHost; 446 | scrollHost.addEventListener(SCROLL_EVENT_NAME, this.scrollHandler); 447 | scrollHost.addEventListener(RESIZE_EVENT_NAME, this.scrollHandler); 448 | this.scrollDirection = scrollHostInfo.scrollDirection; 449 | }; 450 | VirtualizedScrollViewer.prototype.removeScrollHandler = function () { 451 | var scrollHost = this.getScrollHostInfo().scrollHost; 452 | scrollHost.removeEventListener(SCROLL_EVENT_NAME, this.scrollHandler); 453 | scrollHost.removeEventListener(RESIZE_EVENT_NAME, this.scrollHandler); 454 | }; 455 | VirtualizedScrollViewer.prototype.componentDidMount = function () { 456 | var _this = this; 457 | this.itemsContainer = ReactDOM.findDOMNode(this); 458 | var onWindowScroll = function () { 459 | window.removeEventListener(SCROLL_EVENT_NAME, onWindowScroll, true); 460 | window.removeEventListener(RESIZE_EVENT_NAME, onWindowScroll, true); 461 | _this.addScrollHandler(); 462 | }; 463 | requestAnimationFrame(function () { 464 | if (!_this.isDisposed) { 465 | window.addEventListener(SCROLL_EVENT_NAME, onWindowScroll, true); 466 | window.addEventListener(RESIZE_EVENT_NAME, onWindowScroll, true); 467 | } 468 | }); 469 | this.setState(this.getCurrentScrollViewerState(this.props.length)); 470 | }; 471 | VirtualizedScrollViewer.prototype.componentWillUnmount = function () { 472 | this.removeScrollHandler(); 473 | this.scrollHostInfo = null; 474 | this.itemsContainer = null; 475 | }; 476 | VirtualizedScrollViewer.prototype.componentWillReceiveProps = function (nextProps) { 477 | this.setState(this.getCurrentScrollViewerState(nextProps.length)); 478 | this.hasPendingPropertiesUpdate = true; 479 | }; 480 | VirtualizedScrollViewer.prototype.componentWillUpdate = function (nextProps, nextState) { 481 | if (Math.abs(nextState.averageItemSize - this.state.averageItemSize) > 30) { 482 | this.itemsPlaceholdersImageDataUri = null; 483 | } 484 | }; 485 | VirtualizedScrollViewer.prototype.setState = function (state, callback) { 486 | var _this = this; 487 | _super.prototype.setState.call(this, state, function () { 488 | _this.onDidUpdate(); 489 | if (callback) { 490 | callback(); 491 | } 492 | }); 493 | }; 494 | VirtualizedScrollViewer.prototype.onDidUpdate = function () { 495 | var _this = this; 496 | this.itemsContainer = ReactDOM.findDOMNode(this); 497 | this.renderOffScreenBuffer(); 498 | if (this.setPendingScroll) { 499 | requestAnimationFrame(function () { 500 | if (!_this.isDisposed) { 501 | _this.setPendingScroll(); 502 | _this.setPendingScroll = null; 503 | } 504 | }); 505 | } 506 | if (!this.isComponentInitialized) { 507 | this.isComponentInitialized = true; 508 | if (this.props.initializationCompleted) { 509 | this.props.initializationCompleted(); 510 | } 511 | } 512 | if (this.hasPendingPropertiesUpdate) { 513 | this.hasPendingPropertiesUpdate = false; 514 | this.setState(this.getCurrentScrollViewerState(this.props.length)); 515 | } 516 | }; 517 | VirtualizedScrollViewer.prototype.renderOffScreenBuffer = function () { 518 | this.itemsContainer.style.position = "relative"; 519 | var items = this.getListItems(this.itemsContainer); 520 | for (var _i = 0, _a = items.slice(0, this.state.offScreenItemsCount); _i < _a.length; _i++) { 521 | var item = _a[_i]; 522 | var child = item; 523 | if (child.style !== undefined) { 524 | child.style.position = "absolute"; 525 | child.style.top = "-10000" + PIXEL_UNITS; 526 | } 527 | } 528 | for (var _b = 0, _c = items.slice(this.state.offScreenItemsCount); _b < _c.length; _b++) { 529 | var item = _c[_b]; 530 | var child = item; 531 | if (child.style !== undefined) { 532 | child.style.position = ""; 533 | child.style.top = ""; 534 | } 535 | } 536 | }; 537 | VirtualizedScrollViewer.prototype.handleScroll = function (scrollEvent) { 538 | var _this = this; 539 | if (this.pendingScrollAsyncUpdateHandle) { 540 | return; 541 | } 542 | this.pendingScrollAsyncUpdateHandle = requestAnimationFrame(function () { 543 | if (_this.isDisposed) { 544 | return; 545 | } 546 | try { 547 | var newState = _this.getCurrentScrollViewerState(_this.props.length, true); 548 | if (newState !== _this.state) { 549 | _this.isScrollOngoing = true; 550 | _this.setState(newState, function () { return _this.isScrollOngoing = false; }); 551 | } 552 | } 553 | finally { 554 | _this.pendingScrollAsyncUpdateHandle = 0; 555 | } 556 | if (_this.props.scrollChanged) { 557 | _this.props.scrollChanged(); 558 | } 559 | }); 560 | }; 561 | VirtualizedScrollViewer.prototype.shouldComponentUpdate = function (nextProps, nextState) { 562 | return nextState.firstRenderedItemIndex !== this.state.firstRenderedItemIndex || 563 | nextState.lastRenderedItemIndex !== this.state.lastRenderedItemIndex || 564 | nextState.scrollOffset !== this.state.scrollOffset || 565 | nextProps !== this.props; 566 | }; 567 | VirtualizedScrollViewer.prototype.renderList = function (firstRenderedItemIndex, lastRenderedItemIndex) { 568 | var scrollOffset = this.state.scrollOffset; 569 | var length = Math.min(this.props.length, lastRenderedItemIndex - firstRenderedItemIndex + 1); 570 | var items = this.props.renderItems(firstRenderedItemIndex, length); 571 | var remainingSize = 0; 572 | var averageItemSize = Math.max(MIN_ITEM_SIZE, this.state.averageItemSize); 573 | if (lastRenderedItemIndex < (this.props.length - 1)) { 574 | var scrollSize = averageItemSize * this.props.length; 575 | remainingSize = scrollSize - ((averageItemSize * length) + scrollOffset); 576 | } 577 | var listChildren = []; 578 | listChildren.push(this.renderSpacer("first-spacer", scrollOffset, averageItemSize)); 579 | listChildren.push(items); 580 | listChildren.push(this.renderSpacer("last-spacer", remainingSize, averageItemSize)); 581 | return this.props.renderWrapper(listChildren); 582 | }; 583 | VirtualizedScrollViewer.prototype.renderSpacer = function (key, dimension, averageItemSize) { 584 | var FILL_SPACE = "100%"; 585 | var style = { 586 | display: FLEXBOX_DISPLAY, 587 | }; 588 | var backgroundWidth = 0; 589 | var backgroundHeight = 0; 590 | if (this.scrollDirection === virtualized_scroll_viewer_extensions_2.ScrollExtensions.ScrollDirection.Horizontal) { 591 | style.width = Math.round(dimension) + PIXEL_UNITS; 592 | style.height = FILL_SPACE; 593 | backgroundWidth = averageItemSize; 594 | } 595 | else { 596 | style.width = FILL_SPACE; 597 | style.height = Math.round(dimension) + PIXEL_UNITS; 598 | backgroundHeight = averageItemSize; 599 | } 600 | style.backgroundImage = "url(" + this.getItemsPlaceholdersImage(backgroundWidth, backgroundHeight) + ")"; 601 | style.backgroundRepeat = "repeat"; 602 | return React.DOM.script({ key: key, style: style }); 603 | }; 604 | VirtualizedScrollViewer.prototype.render = function () { 605 | return this.renderList(this.state.firstRenderedItemIndex, this.state.lastRenderedItemIndex); 606 | }; 607 | VirtualizedScrollViewer.prototype.getDimension = function (vertical, horizontal) { 608 | return this.scrollDirection === virtualized_scroll_viewer_extensions_2.ScrollExtensions.ScrollDirection.Horizontal ? horizontal : vertical; 609 | }; 610 | VirtualizedScrollViewer.prototype.getListItems = function (itemsContainer) { 611 | var items = []; 612 | for (var i = 1; i < itemsContainer.children.length - 1; i++) { 613 | items.push(itemsContainer.children[i]); 614 | } 615 | return items; 616 | }; 617 | VirtualizedScrollViewer.prototype.getItemBounds = function (item) { 618 | var bounds = item.getBoundingClientRect(); 619 | var rect = { 620 | width: bounds.width, 621 | height: bounds.height, 622 | left: bounds.left, 623 | right: bounds.right, 624 | top: bounds.top, 625 | bottom: bounds.bottom, 626 | }; 627 | if (this.scrollDirection === virtualized_scroll_viewer_extensions_2.ScrollExtensions.ScrollDirection.Horizontal) { 628 | if (rect.width < MIN_ITEM_SIZE) { 629 | rect.width = MIN_ITEM_SIZE; 630 | rect.right = rect.left + rect.width; 631 | } 632 | } 633 | else { 634 | if (rect.height < MIN_ITEM_SIZE) { 635 | rect.height = MIN_ITEM_SIZE; 636 | rect.bottom = rect.top + rect.height; 637 | } 638 | } 639 | return rect; 640 | }; 641 | VirtualizedScrollViewer.prototype.areElementsStacked = function (items) { 642 | if (items.length < 2) { 643 | return false; 644 | } 645 | var firstElement = items[items.length - 2]; 646 | var secondElement = items[items.length - 1]; 647 | var firstElementBounds = firstElement.getBoundingClientRect(); 648 | var secondElementBounds = secondElement.getBoundingClientRect(); 649 | return Math.floor(this.getDimension(secondElementBounds.top, 0)) >= Math.floor(this.getDimension(firstElementBounds.bottom, 1)); 650 | }; 651 | VirtualizedScrollViewer.prototype.calculateItemsSize = function (items, firstItemIndex, lastItemIndex) { 652 | if (firstItemIndex === void 0) { firstItemIndex = 0; } 653 | if (lastItemIndex === void 0) { lastItemIndex = items.length - 1; } 654 | var total = 0; 655 | var sizes = new Array(lastItemIndex - firstItemIndex + 1); 656 | for (var i = firstItemIndex; i <= lastItemIndex; i++) { 657 | var itemBounds = this.getItemBounds(items[i]); 658 | var size = this.getDimension(itemBounds.height, itemBounds.width); 659 | total += size; 660 | sizes[i - firstItemIndex] = size; 661 | } 662 | return { total: total, sizes: sizes }; 663 | }; 664 | VirtualizedScrollViewer.prototype.countItemsAndSizeThatFitIn = function (itemsSizes, sizeToFit, allowOverflow, countBackwards) { 665 | if (allowOverflow === void 0) { allowOverflow = false; } 666 | if (countBackwards === void 0) { countBackwards = false; } 667 | var i = 0; 668 | var itemsSize = 0; 669 | var getIndex = countBackwards ? function (idx) { return itemsSizes.length - 1 - idx; } : function (idx) { return idx; }; 670 | for (; i < itemsSizes.length; i++) { 671 | var itemSize = itemsSizes[getIndex(i)]; 672 | if ((itemsSize + itemSize) > sizeToFit) { 673 | if (allowOverflow) { 674 | i++; 675 | itemsSize += itemSize; 676 | } 677 | break; 678 | } 679 | itemsSize += itemSize; 680 | } 681 | return { size: itemsSize, count: i }; 682 | }; 683 | VirtualizedScrollViewer.prototype.getCurrentScrollViewerState = function (listLength, returnSameStateOnSmallChanges) { 684 | if (returnSameStateOnSmallChanges === void 0) { returnSameStateOnSmallChanges = false; } 685 | var scrollInfo = this.getScrollInfo(); 686 | var pageBufferSize = (this.props.pageBufferSize || DEFAULT_BUFFER_SIZE) * BUFFER_MULTIPLIER; 687 | var viewportSafetyMargin = scrollInfo.viewportSize * (pageBufferSize / 2); 688 | if (returnSameStateOnSmallChanges && 689 | Math.abs(scrollInfo.scrollOffset - this.state.effectiveScrollOffset) < (viewportSafetyMargin * 0.5)) { 690 | return this.state; 691 | } 692 | var items = this.getListItems(this.itemsContainer); 693 | if (!this.areElementsStacked(items)) { 694 | return { 695 | firstRenderedItemIndex: 0, 696 | lastRenderedItemIndex: Math.max(1, this.props.length - 1), 697 | averageItemSize: 0, 698 | scrollOffset: 0, 699 | offScreenItemsCount: 0, 700 | effectiveScrollOffset: scrollInfo.scrollOffset, 701 | }; 702 | } 703 | var lastSpacerBounds = this.itemsContainer.lastElementChild.getBoundingClientRect(); 704 | if (this.getDimension(lastSpacerBounds.bottom, lastSpacerBounds.right) < -100) { 705 | return this.state; 706 | } 707 | var renderedItemsSizes = this.calculateItemsSize(items); 708 | var offScreenItemsCount = this.state.offScreenItemsCount; 709 | var onScreenItems = renderedItemsSizes.sizes.slice(offScreenItemsCount); 710 | var onScreenItemsSize = onScreenItems.reduce(function (p, c) { return p + c; }); 711 | var averageItemSize = onScreenItemsSize / (onScreenItems.length * 1.0); 712 | if (this.state.averageItemSize !== 0) { 713 | averageItemSize = (0.8 * this.state.averageItemSize) + (0.2 * averageItemSize); 714 | } 715 | var itemsFittingViewportCount = Math.ceil(scrollInfo.viewportSize / averageItemSize); 716 | var maxOffScreenItemsCount = itemsFittingViewportCount; 717 | var safetyItemsCount = Math.ceil(viewportSafetyMargin * 2 / averageItemSize); 718 | var renderedItemsCount = Math.min(listLength, itemsFittingViewportCount + safetyItemsCount + maxOffScreenItemsCount); 719 | var scrollOffset = this.state.scrollOffset; 720 | var firstRenderedItemIndex = this.state.firstRenderedItemIndex; 721 | var viewportLowerMargin = scrollInfo.viewportLowerBound - viewportSafetyMargin; 722 | var firstSpacerBounds = this.itemsContainer.firstElementChild.getBoundingClientRect(); 723 | var firstItemOffset = this.getDimension(firstSpacerBounds.bottom, firstSpacerBounds.right); 724 | if (Math.abs(firstItemOffset - viewportLowerMargin) <= onScreenItemsSize) { 725 | if (firstItemOffset < viewportLowerMargin) { 726 | var itemsGoingOffScreen = this.countItemsAndSizeThatFitIn(onScreenItems, Math.abs(viewportLowerMargin - firstItemOffset)); 727 | if (itemsGoingOffScreen.count > 0) { 728 | scrollOffset += itemsGoingOffScreen.size; 729 | offScreenItemsCount += itemsGoingOffScreen.count; 730 | if (offScreenItemsCount > maxOffScreenItemsCount) { 731 | var leavingItemsCount = offScreenItemsCount - maxOffScreenItemsCount; 732 | firstRenderedItemIndex += leavingItemsCount; 733 | offScreenItemsCount = maxOffScreenItemsCount; 734 | } 735 | } 736 | } 737 | else if (firstItemOffset > viewportLowerMargin) { 738 | var availableSpace = Math.abs(firstItemOffset - viewportLowerMargin); 739 | var offScreenItems = renderedItemsSizes.sizes.slice(0, offScreenItemsCount); 740 | var itemsGoingOnScreen = this.countItemsAndSizeThatFitIn(offScreenItems, availableSpace, true, true); 741 | if (itemsGoingOnScreen.count > 0) { 742 | scrollOffset = Math.max(0, scrollOffset - itemsGoingOnScreen.size); 743 | offScreenItemsCount -= itemsGoingOnScreen.count; 744 | availableSpace -= itemsGoingOnScreen.size; 745 | } 746 | if (availableSpace > 0) { 747 | if (offScreenItemsCount !== 0) { 748 | throw "offScreenItemsCount should be 0"; 749 | } 750 | var enteringItemsCount = Math.min(firstRenderedItemIndex, Math.ceil(availableSpace / averageItemSize)); 751 | firstRenderedItemIndex -= enteringItemsCount; 752 | scrollOffset -= enteringItemsCount * averageItemSize; 753 | } 754 | if (offScreenItemsCount < maxOffScreenItemsCount) { 755 | var enteringItemsCount = Math.min(firstRenderedItemIndex, maxOffScreenItemsCount - offScreenItemsCount); 756 | firstRenderedItemIndex -= enteringItemsCount; 757 | offScreenItemsCount += enteringItemsCount; 758 | } 759 | } 760 | } 761 | else { 762 | var startOffset = this.getDimension(firstSpacerBounds.top, firstSpacerBounds.left); 763 | if (startOffset < scrollInfo.viewportLowerBound) { 764 | startOffset = Math.abs(startOffset - scrollInfo.viewportLowerBound); 765 | } 766 | else { 767 | startOffset = 0; 768 | } 769 | firstRenderedItemIndex = Math.max(0, Math.floor(startOffset / averageItemSize) - 1); 770 | offScreenItemsCount = 0; 771 | if (firstRenderedItemIndex > 0) { 772 | firstRenderedItemIndex = Math.max(0, firstRenderedItemIndex - Math.ceil(viewportSafetyMargin / averageItemSize)); 773 | } 774 | firstRenderedItemIndex = Math.max(0, Math.min(firstRenderedItemIndex, listLength - 1 - renderedItemsCount)); 775 | scrollOffset = firstRenderedItemIndex * averageItemSize; 776 | } 777 | if (firstRenderedItemIndex === 0 && offScreenItemsCount === 0) { 778 | scrollOffset = 0; 779 | } 780 | var lastRenderedItemIndex = Math.min(listLength - 1, firstRenderedItemIndex + renderedItemsCount); 781 | return { 782 | firstRenderedItemIndex: firstRenderedItemIndex, 783 | lastRenderedItemIndex: lastRenderedItemIndex, 784 | averageItemSize: averageItemSize, 785 | scrollOffset: scrollOffset, 786 | offScreenItemsCount: offScreenItemsCount, 787 | effectiveScrollOffset: scrollInfo.scrollOffset, 788 | }; 789 | }; 790 | Object.defineProperty(VirtualizedScrollViewer.prototype, "isScrolling", { 791 | get: function () { 792 | return this.isScrollOngoing; 793 | }, 794 | enumerable: true, 795 | configurable: true 796 | }); 797 | Object.defineProperty(VirtualizedScrollViewer.prototype, "isInitialized", { 798 | get: function () { 799 | return this.isComponentInitialized; 800 | }, 801 | enumerable: true, 802 | configurable: true 803 | }); 804 | Object.defineProperty(VirtualizedScrollViewer.prototype, "isDisposed", { 805 | get: function () { 806 | return !this.itemsContainer; 807 | }, 808 | enumerable: true, 809 | configurable: true 810 | }); 811 | VirtualizedScrollViewer.prototype.setScrollOffset = function (x, y) { 812 | var scrollInfo = this.getScrollInfo(); 813 | var scrollHost = scrollInfo.scrollHost; 814 | var scrollX = this.getDimension(undefined, x); 815 | var scrollY = this.getDimension(y, undefined); 816 | var updateScroll = function () { virtualized_scroll_viewer_extensions_2.ScrollExtensions.setScrollOffset(scrollHost, scrollX, scrollY); }; 817 | if (this.isInitialized) { 818 | updateScroll(); 819 | } 820 | else { 821 | this.setPendingScroll = updateScroll; 822 | } 823 | }; 824 | VirtualizedScrollViewer.prototype.getItemsPlaceholdersImage = function (width, height) { 825 | if (!this.itemsPlaceholdersImageDataUri) { 826 | this.itemsPlaceholdersImageDataUri = this.drawItemsPlaceholders(width, height); 827 | } 828 | return this.itemsPlaceholdersImageDataUri; 829 | }; 830 | VirtualizedScrollViewer.prototype.drawItemsPlaceholders = function (width, height) { 831 | var minWidth = Math.max(width, 1); 832 | var minHeight = Math.max(height, 1); 833 | var canvas = document.createElement("canvas"); 834 | canvas.width = Math.max(width * 2, 1); 835 | canvas.height = Math.max(height * 2, 1); 836 | var ctx = canvas.getContext("2d"); 837 | ctx.fillStyle = "rgba(0, 0, 0, 0.04)"; 838 | ctx.fillRect(0, 0, minWidth, minHeight); 839 | ctx.fillStyle = "rgba(0, 0, 0, 0.08)"; 840 | ctx.fillRect(width, height, minWidth, minHeight); 841 | return canvas.toDataURL(); 842 | }; 843 | return VirtualizedScrollViewer; 844 | }(React.Component)); 845 | exports.VirtualizedScrollViewer = VirtualizedScrollViewer; 846 | }); 847 | define("virtualized-list", ["require", "exports", "react", "virtualized-scroll-viewer", "animated-size-group"], function (require, exports, React, virtualized_scroll_viewer_1, animated_size_group_1) { 848 | "use strict"; 849 | var SCROLL_VIEWER_COMPONENT_REF = "scrollViewer"; 850 | var VirtualizedList = (function (_super) { 851 | __extends(VirtualizedList, _super); 852 | function VirtualizedList() { 853 | _super.apply(this, arguments); 854 | } 855 | VirtualizedList.prototype.renderItem = function (index) { 856 | var even = index % 2 === 0; 857 | var className = "list-item " + (even ? "even" : "odd"); 858 | var item = this.props.list[index]; 859 | return (React.createElement("div", {key: "i-" + index, className: className}, "Item ", item.index, React.createElement("img", {src: item.image}))); 860 | }; 861 | VirtualizedList.prototype.componentDidMount = function () { 862 | this.getScrollViewer().setScrollOffset(0, 1000); 863 | }; 864 | VirtualizedList.prototype.renderItems = function (startIndex, length) { 865 | var items = []; 866 | for (var i = startIndex; i < startIndex + length; i++) { 867 | items.push(this.renderItem(i)); 868 | } 869 | return items; 870 | }; 871 | VirtualizedList.prototype.getScrollViewer = function () { 872 | return this.refs[SCROLL_VIEWER_COMPONENT_REF]; 873 | }; 874 | Object.defineProperty(VirtualizedList.prototype, "shouldSuspendAnimations", { 875 | get: function () { 876 | var scrollViewer = this.getScrollViewer(); 877 | return !scrollViewer || !scrollViewer.isInitialized || scrollViewer.isScrolling; 878 | }, 879 | enumerable: true, 880 | configurable: true 881 | }); 882 | VirtualizedList.prototype.createScrollViewerContainer = function (children) { 883 | var _this = this; 884 | var listAttributes = { 885 | className: "list", 886 | component: "div", 887 | shouldSuspendAnimations: function () { return _this.shouldSuspendAnimations; }, 888 | transitionName: "example", 889 | }; 890 | return React.createElement(animated_size_group_1.AnimatedSizeGroup, listAttributes, children); 891 | }; 892 | VirtualizedList.prototype.render = function () { 893 | var _this = this; 894 | return (React.createElement(virtualized_scroll_viewer_1.VirtualizedScrollViewer, {renderItems: function (start, length) { return _this.renderItems(start, length); }, renderWrapper: function (children) { return _this.createScrollViewerContainer(children); }, length: this.props.list.length, pageBufferSize: this.props.pageBufferSize, ref: SCROLL_VIEWER_COMPONENT_REF})); 895 | }; 896 | VirtualizedList.prototype.setScrollOffset = function (offset) { 897 | this.getScrollViewer().setScrollOffset(undefined, offset); 898 | }; 899 | return VirtualizedList; 900 | }(React.Component)); 901 | exports.VirtualizedList = VirtualizedList; 902 | }); 903 | define("index", ["require", "exports", "react", "react-dom", "virtualized-list", "images"], function (require, exports, React, ReactDOM, virtualized_list_1, images_1) { 904 | "use strict"; 905 | var App = (function (_super) { 906 | __extends(App, _super); 907 | function App() { 908 | _super.call(this); 909 | this.state = { 910 | items: 100, 911 | pageBufferSize: 4 912 | }; 913 | } 914 | App.prototype.refresh = function () { 915 | this.setState({ 916 | items: parseInt(this.refs["itemsCount"].value), 917 | pageBufferSize: parseInt(this.refs["pageBufferSize"].value) 918 | }); 919 | }; 920 | App.prototype.setScroll = function () { 921 | var offset = parseInt(this.refs["scrollOffset"].value); 922 | this.refs["list"].setScrollOffset(offset); 923 | }; 924 | App.prototype.render = function () { 925 | var imagesCount = images_1.Images.length; 926 | var list = []; 927 | for (var i = 0; i < this.state.items; i++) { 928 | list.push({ image: images_1.Images[i % imagesCount], index: i }); 929 | } 930 | return (React.createElement("div", null, React.createElement("h1", null, "Virtualized list example"), React.createElement("br", null), React.createElement("input", {ref: "itemsCount", placeholder: "Number of items", defaultValue: this.state.items + ""}), React.createElement("button", {onClick: this.refresh.bind(this)}, "Set Items"), React.createElement("br", null), React.createElement("br", null), React.createElement("input", {ref: "pageBufferSize", placeholder: "Number extra invisible of items rendered", defaultValue: this.state.pageBufferSize + ""}), React.createElement("button", {onClick: this.refresh.bind(this)}, "Set Buffer Size"), React.createElement("br", null), React.createElement("br", null), React.createElement("input", {ref: "scrollOffset", placeholder: "Scroll offset", defaultValue: this.state.items + ""}), React.createElement("button", {onClick: this.setScroll.bind(this)}, "Set Scroll"), React.createElement("br", null), React.createElement("br", null), React.createElement(virtualized_list_1.VirtualizedList, {ref: "list", list: list, pageBufferSize: this.state.pageBufferSize}))); 931 | }; 932 | return App; 933 | }(React.Component)); 934 | ReactDOM.render(React.createElement(App), document.getElementById("container")); 935 | }); 936 | -------------------------------------------------------------------------------- /bin/react-dom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ReactDOM v15.1.0 3 | * 4 | * Copyright 2013-present, Facebook, Inc. 5 | * All rights reserved. 6 | * 7 | * This source code is licensed under the BSD-style license found in the 8 | * LICENSE file in the root directory of this source tree. An additional grant 9 | * of patent rights can be found in the PATENTS file in the same directory. 10 | * 11 | */ 12 | // Based off https://github.com/ForbesLindesay/umd/blob/master/template.js 13 | ;(function(f) { 14 | // CommonJS 15 | if (typeof exports === "object" && typeof module !== "undefined") { 16 | module.exports = f(require('react')); 17 | 18 | // RequireJS 19 | } else if (typeof define === "function" && define.amd) { 20 | define(['react'], f); 21 | 22 | // 6 | 64 | 65 | 66 |
67 | 68 | -------------------------------------------------------------------------------- /libs/react-dom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ReactDOM v15.1.0 3 | * 4 | * Copyright 2013-present, Facebook, Inc. 5 | * All rights reserved. 6 | * 7 | * This source code is licensed under the BSD-style license found in the 8 | * LICENSE file in the root directory of this source tree. An additional grant 9 | * of patent rights can be found in the PATENTS file in the same directory. 10 | * 11 | */ 12 | // Based off https://github.com/ForbesLindesay/umd/blob/master/template.js 13 | ;(function(f) { 14 | // CommonJS 15 | if (typeof exports === "object" && typeof module !== "undefined") { 16 | module.exports = f(require('react')); 17 | 18 | // RequireJS 19 | } else if (typeof define === "function" && define.amd) { 20 | define(['react'], f); 21 | 22 | //