├── .github └── CODEOWNERS ├── .gitignore ├── LICENCE.txt ├── README.md ├── component.json ├── examples ├── galleryscrollers.html ├── horizontalpaged-strict.html ├── horizontalpaged.html ├── startendevent.html ├── verticalcontinuous.html └── wholepage.html ├── lib └── ftscroller.js └── package.json /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Guessed from commit history 2 | * @Financial-Times/apps 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ~$* 2 | .DS_Store 3 | Thumbs.db 4 | Desktop.ini -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012-2014 The Financial Times Ltd. 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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 | # FTScroller 2 | 3 | FTScroller is a library for adding momentum scrolling to web content on devices with a touch interface, compatible across most modern platforms including desktop browsers. Although recently support for `overflow: scroll` (or touch equivalents) has increased, this is often still not implemented in a cross-platform or backwards-compatible way, and with no support for features like snapping. 4 | 5 | FTScroller is developed by [FT Labs](http://labs.ft.com), part of the Financial Times. It is inspired by [Touchscroll](https://github.com/davidaurelio/TouchScroll) and [Zynga Scroller](https://github.com/zynga/scroller), but is a complete rewrite. It is extensively used in the [FT Web App](http://app.ft.com), and was developed to achieve better performance and compatibility, including mouse and touch input. 6 | 7 | 8 | ## Usage 9 | 10 | Include ftscroller.js in your JavaScript bundle or add it to your HTML page like this: 11 | 12 | 13 | 14 | The script must be loaded prior to instantiating a scroller on any element of the page. 15 | 16 | To create a scroller, with a few minimal options: 17 | 18 | ```js 19 | var containerElement, scroller; 20 | 21 | containerElement = document.getElementById('scrollcontainer'); 22 | 23 | scroller = new FTScroller(containerElement, { 24 | scrollbars: false, 25 | scrollingX: false 26 | }); 27 | ``` 28 | 29 | ## Examples 30 | 31 | FTScroller is designed to accommodate a range of use cases. Here are some examples - feel free to copy the code and use as the basis for your own projects. 32 | 33 | * [Vertical continuous list](http://ftlabs.github.com/ftscroller/examples/verticalcontinuous.html) 34 | * [Horizontal paged](http://ftlabs.github.com/ftscroller/examples/horizontalpaged.html) (and a [strict version](http://ftlabs.github.com/ftscroller/examples/horizontalpaged-strict.html), constrained to scrolling one page at a time) 35 | * [Multiple vertical scrollers in a horizontally paged scroller](http://ftlabs.github.com/ftscroller/examples/galleryscrollers.html) 36 | * [Whole page scroller](http://ftlabs.github.com/ftscroller/examples/wholepage.html) 37 | 38 | 39 | ## Options 40 | 41 | Options must be specified at create-time by passing a JSON object as the second argument to the `FTScroller` constructor. 42 | 43 | * `alwaysScroll` Whether to always enable scrolling, even if the content of the scroller is not large enough to spill out of the container. This makes the scroller behave more like an element set to "overflow: scroll", with bouncing always occurring if enabled _(boolean, default false)_ 44 | * `baseAlignments` Determines where to anchor the content when the scroller is set up, specified individually for each axis using a JSON object with the keys `x` and `y`. Valid alignments for each axis are -1 (top or left), 0 (center), and 1 (bottom or right). For example, the default baseAlignments of `{x:-1,y:-1}` will display the scroller initially scrolled to the top left of its range. This also affects the alignment of content where the content is smaller than the scroller. 45 | * `bouncing` Allow scroll bouncing and elasticity near the ends of the range and at snap points _(boolean, default true)_ 46 | * `contentWidth` Define the scrollable width; if not defined, this will match the content width _(numeric, default undefined)_ 47 | * `contentHeight` Define the scrollable height; if not defined, this will match the content height _(numeric, default undefined)_ 48 | * `disabledInputMethods` Define any input methods to disable; on some multi-input devices custom behaviour may be desired for some scrollers. No inputs methods are disabled by default. _(object, default { mouse: false, touch: false, scroll: false, pointer: false, focus: false })_ 49 | * `enableRequestAnimationFrameSupport` FTScroller will use requestAnimationFrame on platforms which support it, which is highly recommended; however this can result in the animation being a further half-frame behind the input method, increasing perceived lag slightly. To disable this, set this property to false. _(boolean, default true)_ 50 | * `flinging` Allow a fast scroll to continue with momentum when released _(boolean, default true)_ 51 | * `hwAccelerationClass` FTScroller uses normal translate properties rather than translate3d to position content when scrolling, and triggers hardware acceleration by adding CSS properties (specifically backface-visibility) to this class on platforms that support it. Adjusting this class allows for negotiating complex CSS inheritance to override the default behaviour of FTScroller if you want to change or disable backing layers/3D acceleration. _(string, default an internal class which triggers backing layers)_ 52 | * `maxFlingDuration` Set the maximum time (ms) that a fling can take to complete once the input has ended _(numeric, default 1000ms)_ 53 | * `scrollbars` Whether to display iOS-style scrollbars (which you can style yourself using `.ftscroller_scrollbar` and `.ftscroller_scrollbarx`/`.ftscroller_scrollbary`) while the content is animating _(boolean, default true)_ 54 | * `scrollBoundary` The initial movement required to trigger a full scroll, in pixels; this is the point at which the scroll is exclusive to this particular FTScroller instance and flings become active _(integer, default 1)_ 55 | * `scrollingClassName` The classname to add to the scroller container when it is being actively scrolled. This is disabled by default as it can cause a CSS relayout if enabled, but allows custom styling in response to scrolls _(string, default not set)_ 56 | * `scrollResponseBoundary` The initial movement required to trigger a visual scroll update, in pixels _(integer, default 1)_ 57 | * `scrollingX` Enable scrolling on the X axis if content is available _(boolean, default true)_ 58 | * `scrollingY` Enable scrolling on the Y axis if content is available _(boolean, default true)_ 59 | * `singlePageScrolls` _(was `paginatedSnap`)_ If snapping is enabled, restricts each scroll movement to one 'page' span. That is, if set to true, it will not be possible to move more than one page in a single movement. _(boolean, default false)_ 60 | * `snapping` Enable snapping of content to defined 'pages' or segments _(boolean, default false)_ 61 | * `snapSizeX` Define the horizontal interval content should snap to, in pixels. If this is not set, snapping will be based on pages corresponding to the container size. _(numeric, default undefined)_ 62 | * `snapSizeY` Define the vertical interval content should snap to, in pixels. If this is not set, snapping will be based on pages corresponding to the container size. _(numeric, default undefined)_ 63 | * `updateOnChanges` Automatically detect changes to the content of the scrollable element and update the scroller dimensions whenever the content changes. This is set to false automatically if `contentWidth` and `contentHeight` are specified _(boolean, default true)_ 64 | * `updateOnWindowResize` Automatically catch changes to the window size and update the dimensions of the scroller. It's advisable to set this to true if the scroller has a flexible width or height based on the viewport size. _(boolean, default false)_ 65 | * `windowScrollingActiveFlag` Whether to use a global property of the window object to control whether to allow scrolling to start or not. If the specified window property is set to a truthy value, the scroller will not react to input. If the property is not truthy, the scroller will set it to itself and will scroll. Where multiple scrollers exist on the same page, this ensures that only one can be used at a time, which is particularly useful for nested scrollers (see [Multiple vertical scrollers in a horizontally paged scroller](examples/galleryscrollers.html)). Note that FTScroller automatically allows only one scroller instance to be scrolled at once; use this flag to coordinate input with other parts of your code. _(string, default not set)_ 66 | * `flingBezier` The bezier curve to use for momentum-like flings. _(CubicBezier, default CubicBezier(0.103, 0.389, 0.307, 0.966))_ 67 | * `bounceDecelerationBezier` The bezier curve to use for deceleration when a fling hits the bounds. _(CubicBezier, default CubicBezier(0, 0.5, 0.5, 1))_ 68 | * `bounceBezier` The bezier curve to use for bouncebacks when the scroll exceeds the bounds. _(CubicBezier, default CubicBezier(0.7, 0, 0.9, 0.6))_ 69 | * `invertScrollWheel` If the scroller is constrained to an x axis, convert y scroll to allow single-axis scroll wheels to scroll constrained content. _(boolean, default true)_ 70 | 71 | ## Public interface 72 | 73 | Once the scroller has been applied to an element, the return value from the constructor is an object that offers a number of public properties, methods and events. 74 | 75 | ### Properties 76 | 77 | * `scrollHeight` Gets the scrollable height of the contained content. **Read only** 78 | * `scrollLeft` Gets or sets the current left scroll offset of the scroller in pixels. When set, will cause the scroller to jump to that position (without animating) 79 | * `scrollTop` Gets or sets the current top scroll offset of the scroller in pixels. When set, will cause the scroller to jump to that position (without animating) 80 | * `scrollWidth` Gets the scrollable width of the contained content. **Read only** 81 | * `segmentCount` When snapping is enabled, returns the number of snap segments (which in many use cases can be considered 'pages'). **Read only**. 82 | * `currentSegment` Returns the index of the current segment, starting from 0. Updated when the scroller comes to rest on a new segment. Applies only when `snapping` is set to true. **Read only**. 83 | * `contentContainerNode` Returns the DOM node that contains the scroller contents, for use if you want to amend the contents. **Read only**. 84 | 85 | ### Methods 86 | 87 | * `addEventListener(eventname, callback)` Attaches a specified function to an FTScroller custom event. Available events are listed in the events section below. The function will be called by FTScroller when the event occurs. 88 | * `destroy(removeElements)` Unbinds all event listeners to prevent circular references preventing items from being deallocated, and clean up references to dom elements. Does not remove scroller markup from the DOM unless the optional `removeElements` argument is set to a truthy value. 89 | * `removeEventListener(eventname, callback)` Removes a previously bound function from an event. The function specified in the mathod must be the same object reference passed in the `addEventListener` call (not a redefinition of the same function code) 90 | * `scrollBy(horizontal, vertical[, duration])` Similar to `scrollTo` but allows scrolling relative to the current position rather than to an absolute offset. 91 | * `scrollTo(left, top[, duration])` Scroll to a specified left and top offet over a specified duration. If duration is zero, no animation will occur. If duration is `true` FTScroller will itself choose a duration based on the distance to be scrolled. The left and top inputs will be constrained by the size of the content and the snap points. If false is supplied for either left or top, that axis will not be scrolled and will retain its current offset. 92 | * `setSnapSize(width, height)` Configures the snapping boundaries within the scrolling element if snapping is active. If this is never called, segment size defaults to the width and height of the scroller, ie. page-at-a-time. 93 | * `updateDimensions(width, height[, nosnap])` Sets the dimensions of the scrollable content. If snapping is enabled, and you wish to disable updates of the snapping grid and prevent the current position from being updated, set `nosnap` to true; it defaults to false if not supplied. 94 | * `setDisabledInputMethods(disabledInputMethods)` Set the input methods to disable. No inputs methods are disabled by default. `(object, default { mouse: false, touch: false, scroll: false, pointer: false, focus: false })` 95 | 96 | ### Prototype methods 97 | 98 | * `getPrependedHTML([excludeXAxis, excludeYAxis, hwAccelerationClass])` - Provides half of the HTML that is used to construct the scroller DOM, for use to save a DOM manipulation on Scroller initialisation (see Tips and tricks below). Optionally the x and y axes can be excluded, or a custom layer backing triggering class can be supplied (see the `hwAccelerationClass` option for the constructor). 99 | * `getAppendedHTML([excludeXAxis, excludeYAxis, hwAccelerationClass, scrollbars])` - Provides the second half of the HTML that is used to construct the scroller DOM, for use to save a DOM manipulation on Scroller initialisation (see Tips and tricks below). Optionally the x and y axes can be excluded, or a custom layer backing triggering class can be supplied (see the `hwAccelerationClass` option for the constructor). Pass a truthy value in for the `scrollbars` parameter if you are enabling scrolling. _Any parameters should match those passed in to `getPrependedHTML`._ 100 | 101 | 102 | ### Events 103 | 104 | Events can be bound with the `addEventListener` method. Events are fired syncronously. Regardless of the event, the listener function will receive a single argument. 105 | 106 | * `scroll` Fired whenever the scroll position changes, continuously if an input type is modifying the position. Passes an object containing `scrollLeft` and `scrollTop` properties matching the new scroll position, eg `{scrollLeft:10, scrollTop:45}` 107 | * `scrollstart` Fired when a scroll movement starts. Passes an object with the same characteristics as `scroll`. 108 | * `scrollend` Fired when a scroll movement ends. Passes an object with the same characteristics as `scroll`. 109 | * `segmentdidchange` Fires on completion of a scroll movement, if the scroller is on a different segment to the one it was on at the start of the movement. Passes an object with `segmentX` and `segmentY` properties. 110 | * `segmentwillchange` Fires as soon as the scroll position crosses a segment boundary, during a scroll movement. Passes an object with `segmentX` and `segmentY` properties. 111 | * `reachedstart` Fires when the scroll position reaches the top or left of a scroller. Passes an object with an `axis` property indicating whether the `x` or `y` axis reached its start position. 112 | * `reachedend` Fires when the scroll position reaches the bottom or right of a scroller. Passes an object with an `axis` property indicating whether the `x` or `y` axis reached its end position. 113 | 114 | ## Compatibility 115 | 116 | FTScroller supports input via mouse or touch on the following browsers: 117 | 118 | * **Apple Mobile Safari** (iOS 3.2+ confirmed, best after 4.0+) 119 | * **Google Android Browser** (Android 2.2+ confirmed) 120 | * **Microsoft Internet Explorer 10** (Windows 8 RTM confirmed) 121 | * **RIM BlackBerry PlayBook** (2.0 confirmed, 1.0 should be fine) 122 | * **Most modern desktop browsers** (IE 9+, FF 4+, Safari 3+, Chrome 1+, Opera 11.5+ - ECMAScript 5 getters and setters are currently used) 123 | 124 | ## Tips and tricks 125 | 126 | * If you are putting together the DOM in JavaScript using HTML or a template, it's advantageous to use the prototype methods for `getPrependedHTML` and `getAppendedHTML` to add the FTScroller HTML at the same time. If the elements are added to the very start and very end of the content within the element passed in to the constructor (ignoring whitespace), the Scroller instantiation will detect this and skip the DOM manipulation step, leading to faster instantiation without an additional layout. 127 | * If scrolling is going to only occur along one axis, setting `scrollingX` or `scrollingY` to `false` as part of the construction options skips creating of the corresponding DOM element, saving memory (particularly when using hardware acceleration). 128 | * Depending on your CSS, you may find that you can't scroll quite to the bottom of your content. This is typically a **[collapsing margin](http://www.complexspiral.com/publications/uncollapsing-margins/)** issue — the contents are measured, then wrapped in additional elements, but the margins spilling _out_ of the contents can't be measured. If you see this, an easy fix is typically to put `overflow: hidden` either your scrollable element or its contents; that will act as a boundary for margins so everything can be measured, and typically has no effect on appearance when applied to the content as the scrollable content won't have dimensional limits. 129 | 130 | 131 | 132 | ## Credits and collaboration 133 | 134 | The lead developer of FTScroller is Rowan Beentje at FT Labs. All open source code released by FT Labs is licenced under the MIT licence. We welcome comments, feedback and suggestions. Please feel free to raise an issue or pull request. Enjoy. 135 | -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ftscroller", 3 | "description": "FTScroller is a cross-browser Javascript/CSS library to allow touch, mouse or scrollwheel scrolling within specified elements, with pagination, snapping and bouncing support.", 4 | "version": "0.7.0", 5 | "main": "lib/ftscroller.js", 6 | "scripts": [ 7 | "lib/ftscroller.js" 8 | ], 9 | "license": "MIT" 10 | } 11 | -------------------------------------------------------------------------------- /examples/galleryscrollers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FT scroller demo: Gallery 6 | 22 | 23 | 24 | 25 |

The container below can be scrolled vertically to read the content of the article, and can be swiped horizontally to move between articles. This example demonstrates a number of features:

26 | 31 |

Three examples of the gallery are shown, to show the different effects of the scrollBoundary and scrollResponseBoundary options.

32 |

Example One

33 |

This example is set up as you might want to set up a genuine gallery. Scrolling responds at once, using a scrollResponseBoundary of 1, but only locks scrolling to that axis after 15 pixels using a scrollBoundary of 15. Panning takes a little more to respond, with a scrollResponseBoundary of 8, and also locks to panning using a scrollBoundary of 20. Scrolls respond instantly but can convert to pans, and scrolling is preferred over panning if the input movement is diagonal.

34 | 87 | 88 |

Example Two

89 |

This example is set up an an example of all FTScroller instances being left at the default settings of scrollResponseBoundary: 1 and scrollBoundary: 1. This is generally undesirable with nested scrollers because finger-based input will often trigger a small scroll as the finger touches the screen, so it's hard to control the intended scroll direction.

90 | 91 | 92 |

Example Three

93 |

This example is set up with large boundaries (30px) for all scrollers or both the scrollResponseBoundary and scrollBoundary settings. This requires a reasonable move before scrolls become locked to an axis, reducing the chance of accidental scrolls or pans, but the equally high response boundary means there is no visual feedback before scrolling is locked, which is also not desirable.

94 | 95 | 96 | 97 | 98 | 99 | 100 | 154 | 155 | -------------------------------------------------------------------------------- /examples/horizontalpaged-strict.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FT scroller demo: horizontal paged, strict 5 | 20 | 21 | 22 |
23 |
24 |
Page 1

Swipe left to scroll the next page into view. No matter how hard you fling...

25 |
Page 2

... you will only be able to go one page at a time (useful for paginated articles).

Moves by direct input - mouse or finger on arger pages, or scrollwheels - can continue to scroll further.

26 |
Page 3
27 |
Page 4
28 |
Page 5
29 |
Page 6
30 |
31 |
32 | 33 | 34 | 35 | 36 | 37 | 46 | 47 | -------------------------------------------------------------------------------- /examples/horizontalpaged.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FT scroller demo: horizontal paged 5 | 20 | 21 | 22 |
23 |
24 |
Page 1

Swipe left to scroll the next page into view. If you swipe quickly...

25 |
Page 2

...lots...

26 |
Page 3

...and lots...

27 |
Page 4

...of pages...

28 |
Page 5

...in one single...

29 |
Page 6

...scroll movement. FTScroller will snap to the nearest page when the scroll animation comes to rest.

30 |
31 |
32 | 33 | 34 | 35 | 36 | 37 | 44 | 45 | -------------------------------------------------------------------------------- /examples/startendevent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FT scroller demo: vertical continuous 5 | 16 | 17 | 18 |
19 |

This example uses a very simple setup: a <div> which is set to a height of 400px, and then an FTScroller invocation on that element. Horizontal scrolling has been disabled for a slight performance improvement as it won't be needed.

20 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 21 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 22 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 23 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 24 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 25 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

26 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 27 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 28 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 29 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 30 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 31 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

32 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 33 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 34 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 35 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 36 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 37 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

38 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 39 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 40 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 41 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 42 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 43 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

44 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 45 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 46 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 47 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 48 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 49 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

50 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 51 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 52 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 53 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 54 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 55 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

56 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 57 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 58 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 59 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 60 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 61 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

62 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 63 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 64 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 65 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 66 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 67 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

68 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 69 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 70 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 71 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 72 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 73 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

74 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 75 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 76 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 77 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 78 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 79 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

80 |
81 |
82 | 83 | 84 | 85 | 86 | 87 | 88 | 109 | 110 | -------------------------------------------------------------------------------- /examples/verticalcontinuous.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FT scroller demo: vertical continuous 5 | 16 | 17 | 18 |
19 |

This example uses a very simple setup: a <div> which is set to a height of 400px, and then an FTScroller invocation on that element. Horizontal scrolling has been disabled for a slight performance improvement as it won't be needed.

20 |

Try clicking this input:

21 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 22 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 23 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 24 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 25 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 26 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

27 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 28 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 29 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 30 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 31 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 32 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

33 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 34 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 35 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 36 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 37 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 38 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

39 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 40 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 41 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 42 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 43 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 44 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

45 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 46 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 47 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 48 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 49 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 50 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

51 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 52 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 53 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 54 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 55 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 56 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

57 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 58 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 59 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 60 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 61 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 62 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

63 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 64 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 65 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 66 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 67 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 68 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

69 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 70 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 71 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 72 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 73 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 74 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

75 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 76 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 77 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 78 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 79 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 80 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

81 |
82 | 83 | 84 | 85 | 86 | 87 | 92 | 93 | -------------------------------------------------------------------------------- /examples/wholepage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FT scroller demo: whole page 5 | 17 | 18 | 19 |
20 |

This entire page is scrolled using FTScroller. This may seem redundant but can be useful for snapping, or content in IFRAMEs if native subframe scrolling is not available or does not provide requried features (such as elasticity). On a desktop or laptop, drag the page up and down to scroll, or use a scrollwheel. On a touch UI, swipe up and down.

21 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

22 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

23 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

24 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

25 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

26 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

27 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

28 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

29 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

30 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

31 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

32 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

33 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

34 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

35 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

36 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

37 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

38 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

39 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

40 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

41 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

42 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

43 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

44 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

45 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

46 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

47 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

48 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

49 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

50 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

51 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

52 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

53 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

54 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

55 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

56 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

57 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

58 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

59 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

60 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

61 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

62 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

63 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

64 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

65 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

66 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

67 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

68 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

69 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

70 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

71 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

72 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

73 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

74 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

75 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

76 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

77 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

78 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

79 |
80 | 81 | 82 | 83 | 84 | 85 | 90 | 91 | -------------------------------------------------------------------------------- /lib/ftscroller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FTScroller: touch and mouse-based scrolling for DOM elements larger than their containers. 3 | * 4 | * While this is a rewrite, it is heavily inspired by two projects: 5 | * 1) Uxebu TouchScroll (https://github.com/davidaurelio/TouchScroll), BSD licensed: 6 | * Copyright (c) 2010 uxebu Consulting Ltd. & Co. KG 7 | * Copyright (c) 2010 David Aurelio 8 | * 2) Zynga Scroller (https://github.com/zynga/scroller), MIT licensed: 9 | * Copyright 2011, Zynga Inc. 10 | * Copyright 2011, Deutsche Telekom AG 11 | * 12 | * Includes CubicBezier: 13 | * 14 | * Copyright (C) 2008 Apple Inc. All Rights Reserved. 15 | * Copyright (C) 2010 David Aurelio. All Rights Reserved. 16 | * Copyright (C) 2010 uxebu Consulting Ltd. & Co. KG. All Rights Reserved. 17 | * 18 | * Redistribution and use in source and binary forms, with or without 19 | * modification, are permitted provided that the following conditions 20 | * are met: 21 | * 1. Redistributions of source code must retain the above copyright 22 | * notice, this list of conditions and the following disclaimer. 23 | * 2. Redistributions in binary form must reproduce the above copyright 24 | * notice, this list of conditions and the following disclaimer in the 25 | * documentation and/or other materials provided with the distribution. 26 | * 27 | * THIS SOFTWARE IS PROVIDED BY APPLE INC., DAVID AURELIO, AND UXEBU 28 | * CONSULTING LTD. & CO. KG ``AS IS'' AND ANY EXPRESS OR IMPLIED 29 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 30 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 31 | * IN NO EVENT SHALL APPLE INC. OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 32 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 33 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 34 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 35 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 36 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 37 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 38 | * POSSIBILITY OF SUCH DAMAGE. 39 | * 40 | * @copyright The Financial Times Ltd [All rights reserved] 41 | * @codingstandard ftlabs-jslint 42 | * @version 0.7.0 43 | */ 44 | /** 45 | * @license FTScroller is (c) 2012 The Financial Times Ltd [All rights reserved] and licensed under the MIT license. 46 | * 47 | * Inspired by Uxebu TouchScroll, (c) 2010 uxebu Consulting Ltd. & Co. KG and David Aurelio, which is BSD licensed (https://github.com/davidaurelio/TouchScroll) 48 | * Inspired by Zynga Scroller, (c) 2011 Zynga Inc and Deutsche Telekom AG, which is MIT licensed (https://github.com/zynga/scroller) 49 | * Includes CubicBezier, (c) 2008 Apple Inc [All rights reserved], (c) 2010 David Aurelio and uxebu Consulting Ltd. & Co. KG. [All rights reserved], which is 2-clause BSD licensed (see above or https://github.com/davidaurelio/TouchScroll). 50 | */ 51 | 52 | /*jslint nomen: true, vars: true, browser: true, continue: true, white: true*/ 53 | /*globals FTScrollerOptions*/ 54 | 55 | var FTScroller, CubicBezier; 56 | 57 | (function () { 58 | 'use strict'; 59 | 60 | // Determine the browser engine and prefix, trying to use the unprefixed version where available. 61 | var _vendorCSSPrefix, _vendorStylePropertyPrefix, _vendorTransformLookup, 62 | _pointerEventsPrefixed, _setPointerCapture, _releasePointerCapture, _lostPointerCapture, _trackPointerEvents, _pointerTypeTouch; 63 | if (document.createElement('div').style.transform !== undefined) { 64 | _vendorCSSPrefix = ''; 65 | _vendorStylePropertyPrefix = ''; 66 | _vendorTransformLookup = 'transform'; 67 | } else if (window.opera && Object.prototype.toString.call(window.opera) === '[object Opera]') { 68 | _vendorCSSPrefix = '-o-'; 69 | _vendorStylePropertyPrefix = 'O'; 70 | _vendorTransformLookup = 'OTransform'; 71 | } else if (document.documentElement.style.MozTransform !== undefined) { 72 | _vendorCSSPrefix = '-moz-'; 73 | _vendorStylePropertyPrefix = 'Moz'; 74 | _vendorTransformLookup = 'MozTransform'; 75 | } else if (document.documentElement.style.webkitTransform !== undefined) { 76 | _vendorCSSPrefix = '-webkit-'; 77 | _vendorStylePropertyPrefix = 'webkit'; 78 | _vendorTransformLookup = '-webkit-transform'; 79 | } else if (typeof navigator.cpuClass === 'string') { 80 | _vendorCSSPrefix = '-ms-'; 81 | _vendorStylePropertyPrefix = 'ms'; 82 | _vendorTransformLookup = '-ms-transform'; 83 | } 84 | 85 | // Pointer Events are unprefixed in IE11 86 | if ('pointerEnabled' in window.navigator) { 87 | _pointerEventsPrefixed = false; 88 | _trackPointerEvents = window.navigator.pointerEnabled; 89 | _setPointerCapture = 'setPointerCapture'; 90 | _releasePointerCapture = 'releasePointerCapture'; 91 | _lostPointerCapture = 'lostpointercapture'; 92 | _pointerTypeTouch = 'touch'; 93 | } else if ('msPointerEnabled' in window.navigator) { 94 | _pointerEventsPrefixed = true; 95 | _trackPointerEvents = window.navigator.msPointerEnabled; 96 | _setPointerCapture = 'msSetPointerCapture'; 97 | _releasePointerCapture = 'msReleasePointerCapture'; 98 | _lostPointerCapture = 'MSLostPointerCapture'; 99 | _pointerTypeTouch = 2; // PointerEvent.MSPOINTER_TYPE_TOUCH = 2 in IE10 100 | } 101 | 102 | // Global flag to determine if any scroll is currently active. This prevents 103 | // issues when using multiple scrollers, particularly when they're nested. 104 | var _ftscrollerMoving = false; 105 | 106 | // Determine whether pointer events or touch events can be used 107 | var _trackTouchEvents = !_trackPointerEvents; 108 | 109 | // Determine whether to use modern hardware acceleration rules or dynamic/toggleable rules. 110 | // Certain older browsers - particularly Android browsers - have problems with hardware 111 | // acceleration, so being able to toggle the behaviour dynamically via a CSS cascade is desirable. 112 | var _useToggleableHardwareAcceleration = false; 113 | if ('hasOwnProperty' in window) { 114 | _useToggleableHardwareAcceleration = !window.hasOwnProperty('ArrayBuffer'); 115 | } 116 | 117 | // Feature detection 118 | var _canClearSelection = (window.Selection && window.Selection.prototype.removeAllRanges); 119 | 120 | // If hardware acceleration is using the standard path, but perspective doesn't seem to be supported, 121 | // 3D transforms likely aren't supported either 122 | if (!_useToggleableHardwareAcceleration && document.createElement('div').style[_vendorStylePropertyPrefix + (_vendorStylePropertyPrefix ? 'P' : 'p') + 'erspective'] === undefined) { 123 | _useToggleableHardwareAcceleration = true; 124 | } 125 | 126 | // Style prefixes 127 | var _transformProperty = _vendorStylePropertyPrefix + (_vendorStylePropertyPrefix ? 'T' : 't') + 'ransform'; 128 | var _transitionProperty = _vendorStylePropertyPrefix + (_vendorStylePropertyPrefix ? 'T' : 't') + 'ransition'; 129 | var _translateRulePrefix = _useToggleableHardwareAcceleration ? 'translate(' : 'translate3d('; 130 | var _transformPrefixes = { x: '', y: '0,' }; 131 | var _transformSuffixes = { x: ',0' + (_useToggleableHardwareAcceleration ? ')' : ',0)'), y: (_useToggleableHardwareAcceleration ? ')' : ',0)') }; 132 | 133 | // Constants. Note that the bezier curve should be changed along with the friction! 134 | var _kFriction = 0.998; 135 | var _kMinimumSpeed = 0.01; 136 | 137 | // Create a global stylesheet to set up stylesheet rules and track dynamic entries 138 | (function () { 139 | var stylesheetContainerNode = document.getElementsByTagName('head')[0] || document.documentElement; 140 | var newStyleNode = document.createElement('style'); 141 | var hardwareAccelerationRule; 142 | var _styleText; 143 | newStyleNode.type = 'text/css'; 144 | 145 | // Determine the hardware acceleration logic to use 146 | if (_useToggleableHardwareAcceleration) { 147 | hardwareAccelerationRule = _vendorCSSPrefix + 'transform-style: preserve-3d;'; 148 | } else { 149 | hardwareAccelerationRule = _vendorCSSPrefix + 'transform: translateZ(0);'; 150 | } 151 | 152 | // Add our rules 153 | _styleText = [ 154 | '.ftscroller_container { overflow: hidden; position: relative; max-height: 100%; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); -ms-touch-action: none }', 155 | '.ftscroller_hwaccelerated { ' + hardwareAccelerationRule + ' }', 156 | '.ftscroller_x, .ftscroller_y { position: relative; min-width: 100%; min-height: 100%; overflow: hidden }', 157 | '.ftscroller_x { display: inline-block }', 158 | '.ftscroller_scrollbar { pointer-events: none; position: absolute; width: 5px; height: 5px; border: 1px solid rgba(255, 255, 255, 0.3); -webkit-border-radius: 3px; border-radius: 6px; opacity: 0; ' + _vendorCSSPrefix + 'transition: opacity 350ms; z-index: 10; -webkit-box-sizing: content-box; -moz-box-sizing: content-box; box-sizing: content-box }', 159 | '.ftscroller_scrollbarx { bottom: 2px; left: 2px }', 160 | '.ftscroller_scrollbary { right: 2px; top: 2px }', 161 | '.ftscroller_scrollbarinner { height: 100%; background: #000; -webkit-border-radius: 2px; border-radius: 4px / 6px }', 162 | '.ftscroller_scrollbar.active { opacity: 0.5; ' + _vendorCSSPrefix + 'transition: none; -o-transition: all 0 none }' 163 | ]; 164 | 165 | if (newStyleNode.styleSheet) { 166 | newStyleNode.styleSheet.cssText = _styleText.join('\n'); 167 | } else { 168 | newStyleNode.appendChild(document.createTextNode(_styleText.join('\n'))); 169 | } 170 | 171 | // Add the stylesheet 172 | stylesheetContainerNode.insertBefore(newStyleNode, stylesheetContainerNode.firstChild); 173 | }()); 174 | 175 | /** 176 | * Master constructor for the scrolling function, including which element to 177 | * construct the scroller in, and any scrolling options. 178 | * Note that app-wide options can also be set using a global FTScrollerOptions 179 | * object. 180 | */ 181 | FTScroller = function (domNode, options) { 182 | var key; 183 | var destroy, setSnapSize, scrollTo, scrollBy, updateDimensions, addEventListener, removeEventListener, setDisabledInputMethods, _startScroll, _updateScroll, _endScroll, _finalizeScroll, _interruptScroll, _flingScroll, _snapScroll, _getSnapPositionForIndexes, _getSnapIndexForPosition, _constrainAndRenderTargetScrollPosition, _limitToBounds, _initializeDOM, _existingDOMValid, _domChanged, _updateDimensions, _updateScrollbarDimensions, _updateElementPosition, _updateSegments, _setAxisPosition, _getPosition, _scheduleAxisPosition, _fireEvent, _childFocused, _modifyDistanceBeyondBounds, _distancesBeyondBounds, _startAnimation, _scheduleRender, _cancelAnimation, _addEventHandlers, _removeEventHandlers, _resetEventHandlers, _onTouchStart, _onTouchMove, _onTouchEnd, _onMouseDown, _onMouseMove, _onMouseUp, _onPointerDown, _onPointerMove, _onPointerUp, _onPointerCancel, _onPointerCaptureEnd, _onClick, _onMouseScroll, _captureInput, _releaseInputCapture, _getBoundingRect; 184 | 185 | 186 | /* Note that actual object instantiation occurs at the end of the closure to avoid jslint errors */ 187 | 188 | 189 | /* Options */ 190 | 191 | var _instanceOptions = { 192 | 193 | // Whether to display scrollbars as appropriate 194 | scrollbars: true, 195 | 196 | // Enable scrolling on the X axis if content is available 197 | scrollingX: true, 198 | 199 | // Enable scrolling on the Y axis if content is available 200 | scrollingY: true, 201 | 202 | // The initial movement required to trigger a scroll, in pixels; this is the point at which 203 | // the scroll is exclusive to this particular FTScroller instance. 204 | scrollBoundary: 1, 205 | 206 | // The initial movement required to trigger a visual indication that scrolling is occurring, 207 | // in pixels. This is enforced to be less than or equal to the scrollBoundary, and is used to 208 | // define when the scroller starts drawing changes in response to an input, even if the scroll 209 | // is not treated as having begun/locked yet. 210 | scrollResponseBoundary: 1, 211 | 212 | // Whether to always enable scrolling, even if the content of the scroller does not 213 | // require the scroller to function. This makes the scroller behave more like an 214 | // element set to "overflow: scroll", with bouncing always occurring if enabled. 215 | alwaysScroll: false, 216 | 217 | // The content width to use when determining scroller dimensions. If this 218 | // is false, the width will be detected based on the actual content. 219 | contentWidth: undefined, 220 | 221 | // The content height to use when determining scroller dimensions. If this 222 | // is false, the height will be detected based on the actual content. 223 | contentHeight: undefined, 224 | 225 | // Enable snapping of content to 'pages' or a pixel grid 226 | snapping: false, 227 | 228 | // Define the horizontal interval of the pixel grid; snapping must be enabled for this to 229 | // take effect. If this is not defined, snapping will use intervals based on container size. 230 | snapSizeX: undefined, 231 | 232 | // Define the vertical interval of the pixel grid; snapping must be enabled for this to 233 | // take effect. If this is not defined, snapping will use intervals based on container size. 234 | snapSizeY: undefined, 235 | 236 | // Control whether snapping should be curtailed to only ever flick to the next page 237 | // and not beyond. Snapping needs to be enabled for this to take effect. 238 | singlePageScrolls: false, 239 | 240 | // Allow scroll bouncing and elasticity near the ends and grid 241 | bouncing: true, 242 | 243 | // Allow a fast scroll to continue with momentum when released 244 | flinging: true, 245 | 246 | // Automatically detects changes to the contained markup and 247 | // updates its dimensions whenever the content changes. This is 248 | // set to false if a contentWidth or contentHeight are supplied. 249 | updateOnChanges: true, 250 | 251 | // Automatically catches changes to the window size and updates 252 | // its dimensions. 253 | updateOnWindowResize: false, 254 | 255 | // The alignment to use if the content is smaller than the container; 256 | // this also applies to initial positioning of scrollable content. 257 | // Valid alignments are -1 (top or left), 0 (center), and 1 (bottom or right). 258 | baseAlignments: { x: -1, y: -1 }, 259 | 260 | // Whether to use a window scroll flag, eg window.foo, to control whether 261 | // to allow scrolling to start or now. If the window flag is set to true, 262 | // this element will not start scrolling; this element will also toggle 263 | // the variable while scrolling 264 | windowScrollingActiveFlag: undefined, 265 | 266 | // Instead of always using translate3d for transforms, a mix of translate3d 267 | // and translate with a hardware acceleration class used to trigger acceleration 268 | // is used; this is to allow CSS inheritance to be used to allow dynamic 269 | // disabling of backing layers on older platforms. 270 | hwAccelerationClass: 'ftscroller_hwaccelerated', 271 | 272 | // While use of requestAnimationFrame is highly recommended on platforms 273 | // which support it, it can result in the animation being a further half-frame 274 | // behind the input method, increasing perceived lag slightly. To disable this, 275 | // set this property to false. 276 | enableRequestAnimationFrameSupport: true, 277 | 278 | // Set the maximum time (ms) that a fling can take to complete; if 279 | // this is not set, flings will complete instantly 280 | maxFlingDuration: 1000, 281 | 282 | // Whether to disable any input methods; on some multi-input devices 283 | // custom behaviour may be desired for some scrollers. Use with care! 284 | disabledInputMethods: { 285 | mouse: false, 286 | touch: false, 287 | scroll: false, 288 | pointer: false, 289 | focus: false 290 | }, 291 | 292 | // Define a scrolling class to be added to the scroller container 293 | // when scrolling is active. Note that this can cause a relayout on 294 | // scroll start if defined, but allows custom styling in response to scrolls 295 | scrollingClassName: undefined, 296 | 297 | // Bezier curves defining the feel of the fling (momentum) deceleration, 298 | // the bounce decleration deceleration (as a fling exceeds the bounds), 299 | // and the bounce bezier (used for bouncing back). 300 | flingBezier: new CubicBezier(0.103, 0.389, 0.307, 0.966), 301 | bounceDecelerationBezier: new CubicBezier(0, 0.5, 0.5, 1), 302 | bounceBezier: new CubicBezier(0.7, 0, 0.9, 0.6), 303 | 304 | // If the scroller is constrained to an x axis, convert y scroll to allow single-axis scroll 305 | // wheels to scroll constrained content. 306 | invertScrollWheel: true 307 | }; 308 | 309 | 310 | /* Local variables */ 311 | 312 | // Cache the DOM node and set up variables for other nodes 313 | var _publicSelf; 314 | var _self = this; 315 | var _scrollableMasterNode = domNode; 316 | var _containerNode; 317 | var _contentParentNode; 318 | var _scrollNodes = { x: null, y: null }; 319 | var _scrollbarNodes = { x: null, y: null }; 320 | 321 | // Dimensions of the container element and the content element 322 | var _metrics = { 323 | container: { x: null, y: null }, 324 | content: { x: null, y: null, rawX: null, rawY: null }, 325 | scrollEnd: { x: null, y: null } 326 | }; 327 | 328 | // Snapping details 329 | var _snapGridSize = { 330 | x: false, 331 | y: false, 332 | userX: false, 333 | userY: false 334 | }; 335 | var _snapIndex = { 336 | x: 0, 337 | y: 0 338 | }; 339 | var _baseSegment = { x: 0, y: 0 }; 340 | var _activeSegment = { x: 0, y: 0 }; 341 | 342 | // Track the identifier of any input being tracked 343 | var _inputIdentifier = false; 344 | var _inputIndex = 0; 345 | var _inputCaptured = false; 346 | 347 | // Current scroll positions and tracking 348 | var _isScrolling = false; 349 | var _isDisplayingScroll = false; 350 | var _isAnimating = false; 351 | var _baseScrollPosition = { x: 0, y: 0 }; 352 | var _lastScrollPosition = { x: 0, y: 0 }; 353 | var _targetScrollPosition = { x: 0, y: 0 }; 354 | var _scrollAtExtremity = { x: null, y: null }; 355 | var _preventClick = false; 356 | var _timeouts = []; 357 | var _hasBeenScrolled = false; 358 | 359 | // Gesture details 360 | var _baseScrollableAxes = {}; 361 | var _scrollableAxes = { x: true, y: true }; 362 | var _gestureStart = { x: 0, y: 0, t: 0 }; 363 | var _cumulativeScroll = { x: 0, y: 0 }; 364 | var _eventHistory = []; 365 | 366 | // Allow certain events to be debounced 367 | var _domChangeDebouncer = false; 368 | var _scrollWheelEndDebouncer = false; 369 | 370 | // Performance switches on browsers supporting requestAnimationFrame 371 | var _animationFrameRequest = false; 372 | var _reqAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame || false; 373 | var _cancelAnimationFrame = window.cancelAnimationFrame || window.cancelRequestAnimationFrame || window.mozCancelAnimationFrame || window.mozCancelRequestAnimationFrame || window.webkitCancelAnimationFrame || window.webkitCancelRequestAnimationFrame || window.msCancelAnimationFrame || window.msCancelRequestAnimationFrame || false; 374 | 375 | // Event listeners 376 | var _eventListeners = { 377 | 'scrollstart': [], 378 | 'scroll': [], 379 | 'scrollend': [], 380 | 'segmentwillchange': [], 381 | 'segmentdidchange': [], 382 | 'reachedstart': [], 383 | 'reachedend': [], 384 | 'scrollinteractionend': [] 385 | }; 386 | 387 | // MutationObserver instance, when supported and if DOM change sniffing is enabled 388 | var _mutationObserver; 389 | 390 | 391 | /* Parsing supplied options */ 392 | 393 | // Override default instance options with global - or closure'd - options 394 | if (typeof FTScrollerOptions === 'object' && FTScrollerOptions) { 395 | for (key in FTScrollerOptions) { 396 | if (FTScrollerOptions.hasOwnProperty(key) && _instanceOptions.hasOwnProperty(key)) { 397 | _instanceOptions[key] = FTScrollerOptions[key]; 398 | } 399 | } 400 | } 401 | 402 | // Override default and global options with supplied options 403 | if (options) { 404 | for (key in options) { 405 | if (options.hasOwnProperty(key)) { 406 | 407 | // If a deprecated flag was passed in, warn, and convert to the new flag name 408 | if ('paginatedSnap' === key) { 409 | console.warn('FTScroller: "paginatedSnap" is deprecated; converting to "singlePageScrolls"'); 410 | _instanceOptions.singlePageScrolls = options.paginatedSnap; 411 | continue; 412 | } 413 | 414 | if (_instanceOptions.hasOwnProperty(key)) { 415 | _instanceOptions[key] = options[key]; 416 | } 417 | } 418 | } 419 | 420 | // If snap grid size options were supplied, store them 421 | if (options.hasOwnProperty('snapSizeX') && !isNaN(options.snapSizeX)) { 422 | _snapGridSize.userX = _snapGridSize.x = options.snapSizeX; 423 | } 424 | if (options.hasOwnProperty('snapSizeY') && !isNaN(options.snapSizeY)) { 425 | _snapGridSize.userY = _snapGridSize.y = options.snapSizeY; 426 | } 427 | 428 | // If content width and height were defined, disable updateOnChanges for performance 429 | if (options.contentWidth && options.contentHeight) { 430 | options.updateOnChanges = false; 431 | } 432 | } 433 | 434 | // Validate the scroll response parameter 435 | _instanceOptions.scrollResponseBoundary = Math.min(_instanceOptions.scrollBoundary, _instanceOptions.scrollResponseBoundary); 436 | 437 | // Update base scrollable axes 438 | if (_instanceOptions.scrollingX) { 439 | _baseScrollableAxes.x = true; 440 | } 441 | if (_instanceOptions.scrollingY) { 442 | _baseScrollableAxes.y = true; 443 | } 444 | 445 | // Only enable animation frame support if the instance options permit it 446 | _reqAnimationFrame = _instanceOptions.enableRequestAnimationFrameSupport && _reqAnimationFrame; 447 | _cancelAnimationFrame = _reqAnimationFrame && _cancelAnimationFrame; 448 | 449 | 450 | /* Scoped Functions */ 451 | 452 | /** 453 | * Unbinds all event listeners to prevent circular references preventing items 454 | * from being deallocated, and clean up references to dom elements. Pass in 455 | * "removeElements" to also remove FTScroller DOM elements for special reuse cases. 456 | */ 457 | destroy = function destroy(removeElements) { 458 | var i, l; 459 | 460 | _removeEventHandlers(); 461 | _cancelAnimation(); 462 | if (_domChangeDebouncer) { 463 | window.clearTimeout(_domChangeDebouncer); 464 | _domChangeDebouncer = false; 465 | } 466 | for (i = 0, l = _timeouts.length; i < l; i = i + 1) { 467 | window.clearTimeout(_timeouts[i]); 468 | } 469 | _timeouts.length = 0; 470 | 471 | // Destroy DOM elements if required 472 | if (removeElements && _scrollableMasterNode) { 473 | while (_contentParentNode.firstChild) { 474 | _scrollableMasterNode.appendChild(_contentParentNode.firstChild); 475 | } 476 | _scrollableMasterNode.removeChild(_containerNode); 477 | } 478 | 479 | _scrollableMasterNode = null; 480 | _containerNode = null; 481 | _contentParentNode = null; 482 | _scrollNodes.x = null; 483 | _scrollNodes.y = null; 484 | _scrollbarNodes.x = null; 485 | _scrollbarNodes.y = null; 486 | for (i in _eventListeners) { 487 | if (_eventListeners.hasOwnProperty(i)) { 488 | _eventListeners[i].length = 0; 489 | } 490 | } 491 | 492 | // If this is currently tracked as a scrolling instance, clear the flag 493 | if (_ftscrollerMoving && _ftscrollerMoving === _self) { 494 | _ftscrollerMoving = false; 495 | if (_instanceOptions.windowScrollingActiveFlag) { 496 | window[_instanceOptions.windowScrollingActiveFlag] = false; 497 | } 498 | } 499 | }; 500 | 501 | /** 502 | * Configures the snapping boundaries within the scrolling element if 503 | * snapping is active. If this is never called, snapping defaults to 504 | * using the bounding box, eg page-at-a-time. 505 | */ 506 | setSnapSize = function setSnapSize(width, height) { 507 | _snapGridSize.userX = width; 508 | _snapGridSize.userY = height; 509 | _snapGridSize.x = width; 510 | _snapGridSize.y = height; 511 | 512 | // Ensure the content dimensions conform to the grid 513 | _metrics.content.x = Math.ceil(_metrics.content.rawX / width) * width; 514 | _metrics.content.y = Math.ceil(_metrics.content.rawY / height) * height; 515 | _metrics.scrollEnd.x = _metrics.container.x - _metrics.content.x; 516 | _metrics.scrollEnd.y = _metrics.container.y - _metrics.content.y; 517 | _updateScrollbarDimensions(); 518 | 519 | // Snap to the new grid if necessary 520 | _snapScroll(); 521 | _updateSegments(true); 522 | }; 523 | 524 | /** 525 | * Scroll to a supplied position, including whether or not to animate the 526 | * scroll and how fast to perform the animation (pass in true to select a 527 | * dynamic duration). The inputs will be constrained to bounds and snapped. 528 | * If false is supplied for a position, that axis will not be scrolled. 529 | */ 530 | scrollTo = function scrollTo(left, top, animationDuration) { 531 | var targetPosition, duration, positions, axis, maxDuration = 0, scrollPositionsToApply = {}; 532 | 533 | // If a manual scroll is in progress, cancel it 534 | _endScroll(Date.now()); 535 | 536 | // Move supplied coordinates into an object for iteration, also inverting the values into 537 | // our coordinate system 538 | positions = { 539 | x: -left, 540 | y: -top 541 | }; 542 | 543 | for (axis in _baseScrollableAxes) { 544 | if (_baseScrollableAxes.hasOwnProperty(axis)) { 545 | targetPosition = positions[axis]; 546 | if (targetPosition === false) { 547 | continue; 548 | } 549 | 550 | // Constrain to bounds 551 | targetPosition = Math.min(0, Math.max(_metrics.scrollEnd[axis], targetPosition)); 552 | 553 | // Snap if appropriate 554 | if (_instanceOptions.snapping && _snapGridSize[axis]) { 555 | targetPosition = Math.round(targetPosition / _snapGridSize[axis]) * _snapGridSize[axis]; 556 | } 557 | 558 | // Get a duration 559 | duration = animationDuration || 0; 560 | if (duration === true) { 561 | duration = Math.sqrt(Math.abs(_baseScrollPosition[axis] - targetPosition)) * 20; 562 | } 563 | 564 | // Trigger the position change 565 | _setAxisPosition(axis, targetPosition, duration); 566 | scrollPositionsToApply[axis] = targetPosition; 567 | maxDuration = Math.max(maxDuration, duration); 568 | } 569 | } 570 | 571 | // If the scroll had resulted in a change in position, perform some additional actions: 572 | if (_baseScrollPosition.x !== positions.x || _baseScrollPosition.y !== positions.y) { 573 | 574 | // Mark a scroll as having ever occurred 575 | _hasBeenScrolled = true; 576 | 577 | // If an animation duration is present, fire a scroll start event and a 578 | // scroll event for any listeners to act on 579 | _fireEvent('scrollstart', _getPosition()); 580 | _fireEvent('scroll', _getPosition()); 581 | } 582 | 583 | if (maxDuration) { 584 | _timeouts.push(setTimeout(function () { 585 | var anAxis; 586 | for (anAxis in scrollPositionsToApply) { 587 | if (scrollPositionsToApply.hasOwnProperty(anAxis)) { 588 | _lastScrollPosition[anAxis] = scrollPositionsToApply[anAxis]; 589 | } 590 | } 591 | _finalizeScroll(); 592 | }, maxDuration)); 593 | } else { 594 | _finalizeScroll(); 595 | } 596 | }; 597 | 598 | /** 599 | * Alter the current scroll position, including whether or not to animate 600 | * the scroll and how fast to perform the animation (pass in true to 601 | * select a dynamic duration). The inputs will be checked against the 602 | * current position. 603 | */ 604 | scrollBy = function scrollBy(horizontal, vertical, animationDuration) { 605 | 606 | // Wrap the scrollTo function for simplicity 607 | scrollTo(parseFloat(horizontal) - _baseScrollPosition.x, parseFloat(vertical) - _baseScrollPosition.y, animationDuration); 608 | }; 609 | 610 | /** 611 | * Provide a public method to detect changes in dimensions for either the content or the 612 | * container. 613 | */ 614 | updateDimensions = function updateDimensions(contentWidth, contentHeight, ignoreSnapScroll) { 615 | options.contentWidth = contentWidth || options.contentWidth; 616 | options.contentHeight = contentHeight || options.contentHeight; 617 | 618 | // Currently just wrap the private API 619 | _updateDimensions(!!ignoreSnapScroll); 620 | }; 621 | 622 | /** 623 | * Add an event handler for a supported event. Current events include: 624 | * scroll - fired whenever the scroll position changes 625 | * scrollstart - fired when a scroll movement starts 626 | * scrollend - fired when a scroll movement ends 627 | * segmentwillchange - fired whenever the segment changes, including during scrolling 628 | * segmentdidchange - fired when a segment has conclusively changed, after scrolling. 629 | */ 630 | addEventListener = function addEventListener(eventname, eventlistener) { 631 | 632 | // Ensure this is a valid event 633 | if (!_eventListeners.hasOwnProperty(eventname)) { 634 | return false; 635 | } 636 | 637 | // Add the listener 638 | _eventListeners[eventname].push(eventlistener); 639 | return true; 640 | }; 641 | 642 | /** 643 | * Remove an event handler for a supported event. The listener must be exactly the same as 644 | * an added listener to be removed. 645 | */ 646 | removeEventListener = function removeEventListener(eventname, eventlistener) { 647 | var i; 648 | 649 | // Ensure this is a valid event 650 | if (!_eventListeners.hasOwnProperty(eventname)) { 651 | return false; 652 | } 653 | 654 | for (i = _eventListeners[eventname].length; i >= 0; i = i - 1) { 655 | if (_eventListeners[eventname][i] === eventlistener) { 656 | _eventListeners[eventname].splice(i, 1); 657 | } 658 | } 659 | return true; 660 | }; 661 | 662 | /** 663 | * Set the input methods to disable. No inputs methods are disabled by default. 664 | * (object, default { mouse: false, touch: false, scroll: false, pointer: false, focus: false }) 665 | */ 666 | setDisabledInputMethods = function setDisabledInputMethods(disabledInputMethods) { 667 | var i, changed; 668 | 669 | for (i in _instanceOptions.disabledInputMethods) { 670 | disabledInputMethods[i] = !!disabledInputMethods[i]; 671 | 672 | if (_instanceOptions.disabledInputMethods[i] !== disabledInputMethods[i]) changed = true; 673 | _instanceOptions.disabledInputMethods[i] = disabledInputMethods[i]; 674 | } 675 | 676 | if (changed) { 677 | _resetEventHandlers(); 678 | } 679 | }; 680 | 681 | /** 682 | * Start a scroll tracking input - this could be mouse, webkit-style touch, 683 | * or ms-style pointer events. 684 | */ 685 | _startScroll = function _startScroll(inputX, inputY, inputTime, rawEvent) { 686 | var triggerScrollInterrupt = _isAnimating; 687 | 688 | // Opera fix 689 | if (inputTime <= 0) { 690 | inputTime = Date.now(); 691 | } 692 | 693 | // If a window scrolling flag is set, and evaluates to true, don't start checking touches 694 | if (_instanceOptions.windowScrollingActiveFlag && window[_instanceOptions.windowScrollingActiveFlag]) { 695 | return false; 696 | } 697 | 698 | // If an animation is in progress, stop the scroll. 699 | if (triggerScrollInterrupt) { 700 | _interruptScroll(); 701 | } else { 702 | 703 | // Allow clicks again, but only if a scroll was not interrupted 704 | _preventClick = false; 705 | } 706 | 707 | // Store the initial event coordinates 708 | _gestureStart.x = inputX; 709 | _gestureStart.y = inputY; 710 | _gestureStart.t = inputTime; 711 | _targetScrollPosition.x = _lastScrollPosition.x; 712 | _targetScrollPosition.y = _lastScrollPosition.y; 713 | 714 | // Clear event history and add the start touch 715 | _eventHistory.length = 0; 716 | _eventHistory.push({ x: inputX, y: inputY, t: inputTime }); 717 | 718 | if (triggerScrollInterrupt) { 719 | _updateScroll(inputX, inputY, inputTime, rawEvent, triggerScrollInterrupt); 720 | } 721 | 722 | return true; 723 | }; 724 | 725 | /** 726 | * Continue a scroll as a result of an updated position 727 | */ 728 | _updateScroll = function _updateScroll(inputX, inputY, inputTime, rawEvent, scrollInterrupt) { 729 | var axis, otherScrollerActive, distancesBeyondBounds; 730 | var initialScroll = false; 731 | var gesture = { 732 | x: inputX - _gestureStart.x, 733 | y: inputY - _gestureStart.y 734 | }; 735 | 736 | // Opera fix 737 | if (inputTime <= 0) { 738 | inputTime = Date.now(); 739 | } 740 | 741 | // Update base target positions 742 | _targetScrollPosition.x = _baseScrollPosition.x + gesture.x; 743 | _targetScrollPosition.y = _baseScrollPosition.y + gesture.y; 744 | 745 | // If scrolling has not yet locked to this scroller, check whether to stop scrolling 746 | if (!_isScrolling) { 747 | 748 | // Check the internal flag to determine if another FTScroller is scrolling 749 | if (_ftscrollerMoving && _ftscrollerMoving !== _self) { 750 | otherScrollerActive = true; 751 | } 752 | 753 | // Otherwise, check the window scrolling flag to see if anything else has claimed scrolling 754 | else if (_instanceOptions.windowScrollingActiveFlag && window[_instanceOptions.windowScrollingActiveFlag]) { 755 | otherScrollerActive = true; 756 | } 757 | 758 | // If another scroller was active, clean up and stop processing. 759 | if (otherScrollerActive) { 760 | _releaseInputCapture(); 761 | _inputIdentifier = false; 762 | if (_isDisplayingScroll) { 763 | _cancelAnimation(); 764 | if (!_snapScroll(true)) { 765 | _finalizeScroll(true); 766 | } 767 | } 768 | return; 769 | } 770 | } 771 | 772 | // If not yet displaying a scroll, determine whether that triggering boundary 773 | // has been exceeded 774 | if (!_isDisplayingScroll) { 775 | 776 | // Determine scroll distance beyond bounds 777 | distancesBeyondBounds = _distancesBeyondBounds(_targetScrollPosition); 778 | 779 | // Check scrolled distance against the boundary limit to see if scrolling can be triggered. 780 | // If the scroll has been interrupted, trigger at once 781 | if (!scrollInterrupt && (!_scrollableAxes.x || Math.abs(gesture.x) < _instanceOptions.scrollResponseBoundary) && (!_scrollableAxes.y || Math.abs(gesture.y) < _instanceOptions.scrollResponseBoundary)) { 782 | return; 783 | } 784 | 785 | // Determine whether to prevent the default scroll event - if the scroll could still 786 | // be triggered, prevent the default to avoid problems (particularly on PlayBook) 787 | if (_instanceOptions.bouncing || scrollInterrupt || (_scrollableAxes.x && gesture.x && distancesBeyondBounds.x < 0) || (_scrollableAxes.y && gesture.y && distancesBeyondBounds.y < 0)) { 788 | rawEvent.preventDefault(); 789 | } 790 | 791 | // If bouncing is disabled, and already at an edge and scrolling beyond the edge, ignore the scroll for 792 | // now - this allows other scrollers to claim if appropriate, allowing nicer nested scrolls. 793 | if (!_instanceOptions.bouncing && !scrollInterrupt && (!_scrollableAxes.x || !gesture.x || distancesBeyondBounds.x > 0) && (!_scrollableAxes.y || !gesture.y || distancesBeyondBounds.y > 0)) { 794 | 795 | // Prevent the original click now that scrolling would be triggered 796 | _preventClick = true; 797 | 798 | return; 799 | } 800 | 801 | // Trigger the start of visual scrolling 802 | _startAnimation(); 803 | _isDisplayingScroll = true; 804 | _hasBeenScrolled = true; 805 | _isAnimating = true; 806 | initialScroll = true; 807 | } else { 808 | 809 | // Prevent the event default. It is safe to call this in IE10 because the event is never 810 | // a window.event, always a "true" event. 811 | rawEvent.preventDefault(); 812 | } 813 | 814 | // If not yet locked to a scroll, determine whether to do so 815 | if (!_isScrolling) { 816 | 817 | // If the gesture distance has exceeded the scroll lock distance, or snapping is active 818 | // and the scroll has been interrupted, enter exclusive scrolling. 819 | if ((scrollInterrupt && _instanceOptions.snapping) || (_scrollableAxes.x && Math.abs(gesture.x) >= _instanceOptions.scrollBoundary) || (_scrollableAxes.y && Math.abs(gesture.y) >= _instanceOptions.scrollBoundary)) { 820 | 821 | _isScrolling = true; 822 | _preventClick = true; 823 | _ftscrollerMoving = _self; 824 | if (_instanceOptions.windowScrollingActiveFlag) { 825 | window[_instanceOptions.windowScrollingActiveFlag] = _self; 826 | } 827 | _fireEvent('scrollstart', _getPosition()); 828 | } 829 | } 830 | 831 | // Capture pointer if necessary 832 | if (_isScrolling) { 833 | _captureInput(); 834 | } 835 | 836 | // Cancel text selections while dragging a cursor 837 | if (_canClearSelection) { 838 | window.getSelection().removeAllRanges(); 839 | } 840 | 841 | // Ensure the target scroll position is affected by bounds and render if needed 842 | _constrainAndRenderTargetScrollPosition(); 843 | 844 | // To aid render/draw coalescing, perform other one-off actions here 845 | if (initialScroll) { 846 | if (gesture.x > 0) { 847 | _baseScrollPosition.x -= _instanceOptions.scrollResponseBoundary; 848 | } else if(gesture.x < 0) { 849 | _baseScrollPosition.x += _instanceOptions.scrollResponseBoundary; 850 | } 851 | 852 | if (gesture.y > 0) { 853 | _baseScrollPosition.y -= _instanceOptions.scrollResponseBoundary; 854 | } else if(gesture.y < 0) { 855 | _baseScrollPosition.y += _instanceOptions.scrollResponseBoundary; 856 | } 857 | 858 | _targetScrollPosition.x = _baseScrollPosition.x + gesture.x; 859 | _targetScrollPosition.y = _baseScrollPosition.y + gesture.y; 860 | 861 | if (_instanceOptions.scrollingClassName) { 862 | _containerNode.className += ' ' + _instanceOptions.scrollingClassName; 863 | } 864 | if (_instanceOptions.scrollbars) { 865 | for (axis in _scrollableAxes) { 866 | if (_scrollableAxes.hasOwnProperty(axis)) { 867 | _scrollbarNodes[axis].className += ' active'; 868 | } 869 | } 870 | } 871 | } 872 | 873 | // Add an event to the event history, keeping it around twenty events long 874 | _eventHistory.push({ x: inputX, y: inputY, t: inputTime }); 875 | if (_eventHistory.length > 30) { 876 | _eventHistory.splice(0, 15); 877 | } 878 | }; 879 | 880 | /** 881 | * Complete a scroll with a final event time if available (it may 882 | * not be, depending on the input type); this may continue the scroll 883 | * with a fling and/or bounceback depending on options. 884 | */ 885 | _endScroll = function _endScroll(inputTime, rawEvent) { 886 | _releaseInputCapture(); 887 | _inputIdentifier = false; 888 | _cancelAnimation(); 889 | 890 | _fireEvent('scrollinteractionend', {}); 891 | 892 | if (!_isScrolling) { 893 | if (!_snapScroll(true) && _isDisplayingScroll) { 894 | _finalizeScroll(true); 895 | } 896 | return; 897 | } 898 | 899 | // Modify the last movement event to include the end event time 900 | _eventHistory[_eventHistory.length - 1].t = inputTime; 901 | 902 | // Update flags 903 | _isScrolling = false; 904 | _isDisplayingScroll = false; 905 | _ftscrollerMoving = false; 906 | if (_instanceOptions.windowScrollingActiveFlag) { 907 | window[_instanceOptions.windowScrollingActiveFlag] = false; 908 | } 909 | 910 | // Stop the event default. It is safe to call this in IE10 because 911 | // the event is never a window.event, always a "true" event. 912 | if (rawEvent) { 913 | rawEvent.preventDefault(); 914 | } 915 | 916 | // Trigger a fling or bounceback if necessary 917 | if (!_flingScroll() && !_snapScroll()) { 918 | _finalizeScroll(); 919 | } 920 | }; 921 | 922 | /** 923 | * Remove the scrolling class, cleaning up display. 924 | */ 925 | _finalizeScroll = function _finalizeScroll(scrollCancelled) { 926 | var i, l, axis, scrollEvent, scrollRegex; 927 | 928 | _isAnimating = false; 929 | _isDisplayingScroll = false; 930 | 931 | // Remove scrolling class if set 932 | if (_instanceOptions.scrollingClassName) { 933 | scrollRegex = new RegExp('(?:^|\\s)' + _instanceOptions.scrollingClassName + '(?!\\S)', 'g'); 934 | _containerNode.className = _containerNode.className.replace(scrollRegex, ''); 935 | } 936 | if (_instanceOptions.scrollbars) { 937 | for (axis in _scrollableAxes) { 938 | if (_scrollableAxes.hasOwnProperty(axis)) { 939 | _scrollbarNodes[axis].className = _scrollbarNodes[axis].className.replace(/ ?active/g, ''); 940 | } 941 | } 942 | } 943 | 944 | // Store final position if scrolling occurred 945 | _baseScrollPosition.x = _lastScrollPosition.x; 946 | _baseScrollPosition.y = _lastScrollPosition.y; 947 | 948 | scrollEvent = _getPosition(); 949 | 950 | if (!scrollCancelled) { 951 | _fireEvent('scroll', scrollEvent); 952 | _updateSegments(true); 953 | } 954 | 955 | // Always fire the scroll end event, including an argument indicating whether 956 | // the scroll was cancelled 957 | scrollEvent.cancelled = scrollCancelled; 958 | _fireEvent('scrollend', scrollEvent); 959 | 960 | // Restore transitions 961 | for (axis in _scrollableAxes) { 962 | if (_scrollableAxes.hasOwnProperty(axis)) { 963 | _scrollNodes[axis].style[_transitionProperty] = ''; 964 | if (_instanceOptions.scrollbars) { 965 | _scrollbarNodes[axis].style[_transitionProperty] = ''; 966 | } 967 | } 968 | } 969 | 970 | // Clear any remaining timeouts 971 | for (i = 0, l = _timeouts.length; i < l; i = i + 1) { 972 | window.clearTimeout(_timeouts[i]); 973 | } 974 | _timeouts.length = 0; 975 | }; 976 | 977 | /** 978 | * Interrupt a current scroll, allowing a start scroll during animation to trigger a new scroll 979 | */ 980 | _interruptScroll = function _interruptScroll() { 981 | var axis, i, l; 982 | 983 | _isAnimating = false; 984 | 985 | // Update the stored base position 986 | _updateElementPosition(); 987 | 988 | // Ensure the parsed positions are set, also clearing transitions 989 | for (axis in _scrollableAxes) { 990 | if (_scrollableAxes.hasOwnProperty(axis)) { 991 | _setAxisPosition(axis, _baseScrollPosition[axis], 16, _instanceOptions.bounceDecelerationBezier); 992 | } 993 | } 994 | 995 | // Update segment tracking if snapping is active 996 | _updateSegments(false); 997 | 998 | // Clear any remaining timeouts 999 | for (i = 0, l = _timeouts.length; i < l; i = i + 1) { 1000 | window.clearTimeout(_timeouts[i]); 1001 | } 1002 | _timeouts.length = 0; 1003 | }; 1004 | 1005 | /** 1006 | * Determine whether a scroll fling or bounceback is required, and set up the styles and 1007 | * timeouts required. 1008 | */ 1009 | _flingScroll = function _flingScroll() { 1010 | var i, axis, movementTime, movementSpeed, lastPosition, comparisonPosition, flingDuration, flingDistance, flingPosition, bounceDelay, bounceDistance, bounceDuration, bounceTarget, boundsBounce, modifiedDistance, flingBezier, timeProportion, boundsCrossDelay, flingStartSegment, beyondBoundsFlingDistance, baseFlingComponent; 1011 | var maxAnimationTime = 0; 1012 | var moveRequired = false; 1013 | var scrollPositionsToApply = {}; 1014 | 1015 | // If we only have the start event available, or flinging is disabled, 1016 | // or the scroll was triggered by a scrollwheel, no action required. 1017 | if (_eventHistory.length === 1 || !_instanceOptions.flinging || _inputIdentifier === 'scrollwheel') { 1018 | return false; 1019 | } 1020 | 1021 | for (axis in _scrollableAxes) { 1022 | if (_scrollableAxes.hasOwnProperty(axis)) { 1023 | bounceDuration = 350; 1024 | bounceDistance = 0; 1025 | boundsBounce = false; 1026 | bounceTarget = false; 1027 | boundsCrossDelay = undefined; 1028 | 1029 | // Re-set a default bezier curve for the animation for potential modification 1030 | flingBezier = _instanceOptions.flingBezier; 1031 | 1032 | // Get the last movement speed, in pixels per millisecond. To do this, look at the events 1033 | // in the last 100ms and average out the speed, using a minimum number of two points. 1034 | lastPosition = _eventHistory[_eventHistory.length - 1]; 1035 | comparisonPosition = _eventHistory[_eventHistory.length - 2]; 1036 | for (i = _eventHistory.length - 3; i >= 0; i = i - 1) { 1037 | if (lastPosition.t - _eventHistory[i].t > 100) { 1038 | break; 1039 | } 1040 | comparisonPosition = _eventHistory[i]; 1041 | } 1042 | 1043 | // Get the last movement time. If this is zero - as can happen with 1044 | // some scrollwheel events on some platforms - increase it to 16ms as 1045 | // if the movement occurred over a single frame at 60fps. 1046 | movementTime = lastPosition.t - comparisonPosition.t; 1047 | if (!movementTime) { 1048 | movementTime = 16; 1049 | } 1050 | 1051 | // Derive the movement speed 1052 | movementSpeed = (lastPosition[axis] - comparisonPosition[axis]) / movementTime; 1053 | 1054 | // If there is little speed, no further action required except for a bounceback, below. 1055 | if (Math.abs(movementSpeed) < _kMinimumSpeed) { 1056 | flingDuration = 0; 1057 | flingDistance = 0; 1058 | 1059 | } else { 1060 | 1061 | 1062 | /* Calculate the fling duration. As per TouchScroll, the speed at any particular 1063 | point in time can be calculated as: 1064 | { speed } = { initial speed } * ({ friction } to the power of { duration }) 1065 | ...assuming all values are in equal pixels/millisecond measurements. As we know the 1066 | minimum target speed, this can be altered to: 1067 | { duration } = log( { speed } / { initial speed } ) / log( { friction } ) 1068 | */ 1069 | 1070 | flingDuration = Math.log(_kMinimumSpeed / Math.abs(movementSpeed)) / Math.log(_kFriction); 1071 | 1072 | 1073 | /* Calculate the fling distance (before any bouncing or snapping). As per 1074 | TouchScroll, the total distance covered can be approximated by summing 1075 | the distance per millisecond, per millisecond of duration - a divergent series, 1076 | and so rather tricky to model otherwise! 1077 | So using values in pixels per millisecond: 1078 | { distance } = { initial speed } * (1 - ({ friction } to the power 1079 | of { duration + 1 }) / (1 - { friction }) 1080 | */ 1081 | 1082 | flingDistance = movementSpeed * (1 - Math.pow(_kFriction, flingDuration + 1)) / (1 - _kFriction); 1083 | } 1084 | 1085 | // Determine a target fling position 1086 | flingPosition = Math.floor(_lastScrollPosition[axis] + flingDistance); 1087 | 1088 | // If bouncing is disabled, and the last scroll position and fling position are both at a bound, 1089 | // reset the fling position to the bound 1090 | if (!_instanceOptions.bouncing) { 1091 | if (_lastScrollPosition[axis] === 0 && flingPosition > 0) { 1092 | flingPosition = 0; 1093 | } else if (_lastScrollPosition[axis] === _metrics.scrollEnd[axis] && flingPosition < _lastScrollPosition[axis]) { 1094 | flingPosition = _lastScrollPosition[axis]; 1095 | } 1096 | } 1097 | 1098 | // In single-page-scroll mode, determine the page to snap to - maximum one page 1099 | // in either direction from the *start* page. 1100 | if (_instanceOptions.singlePageScrolls && _instanceOptions.snapping) { 1101 | flingStartSegment = -_lastScrollPosition[axis] / _snapGridSize[axis]; 1102 | if (_baseSegment[axis] < flingStartSegment) { 1103 | flingStartSegment = Math.floor(flingStartSegment); 1104 | } else { 1105 | flingStartSegment = Math.ceil(flingStartSegment); 1106 | } 1107 | 1108 | // If the target position will end up beyond another page, target that page edge 1109 | if (flingPosition > -(_baseSegment[axis] - 1) * _snapGridSize[axis]) { 1110 | bounceDistance = flingPosition + (_baseSegment[axis] - 1) * _snapGridSize[axis]; 1111 | } else if (flingPosition < -(_baseSegment[axis] + 1) * _snapGridSize[axis]) { 1112 | bounceDistance = flingPosition + (_baseSegment[axis] + 1) * _snapGridSize[axis]; 1113 | 1114 | // Otherwise, if the movement speed was above the minimum velocity, continue 1115 | // in the move direction. 1116 | } else if (Math.abs(movementSpeed) > _kMinimumSpeed) { 1117 | 1118 | // Determine the target segment 1119 | if (movementSpeed < 0) { 1120 | flingPosition = Math.floor(_lastScrollPosition[axis] / _snapGridSize[axis]) * _snapGridSize[axis]; 1121 | } else { 1122 | flingPosition = Math.ceil(_lastScrollPosition[axis] / _snapGridSize[axis]) * _snapGridSize[axis]; 1123 | } 1124 | 1125 | flingDuration = Math.min(_instanceOptions.maxFlingDuration, flingDuration * (flingPosition - _lastScrollPosition[axis]) / flingDistance); 1126 | } 1127 | 1128 | // In non-paginated snapping mode, snap to the nearest grid location to the target 1129 | } else if (_instanceOptions.snapping) { 1130 | bounceDistance = flingPosition - (Math.round(flingPosition / _snapGridSize[axis]) * _snapGridSize[axis]); 1131 | } 1132 | 1133 | // Deal with cases where the target is beyond the bounds 1134 | if (flingPosition - bounceDistance > 0) { 1135 | bounceDistance = flingPosition; 1136 | boundsBounce = true; 1137 | } else if (flingPosition - bounceDistance < _metrics.scrollEnd[axis]) { 1138 | bounceDistance = flingPosition - _metrics.scrollEnd[axis]; 1139 | boundsBounce = true; 1140 | } 1141 | 1142 | // Amend the positions and bezier curve if necessary 1143 | if (bounceDistance) { 1144 | 1145 | // If the fling moves the scroller beyond the normal scroll bounds, and 1146 | // the bounce is snapping the scroll back after the fling: 1147 | if (boundsBounce && _instanceOptions.bouncing && flingDistance) { 1148 | flingDistance = Math.floor(flingDistance); 1149 | 1150 | if (flingPosition > 0) { 1151 | beyondBoundsFlingDistance = flingPosition - Math.max(0, _lastScrollPosition[axis]); 1152 | } else { 1153 | beyondBoundsFlingDistance = flingPosition - Math.min(_metrics.scrollEnd[axis], _lastScrollPosition[axis]); 1154 | } 1155 | baseFlingComponent = flingDistance - beyondBoundsFlingDistance; 1156 | 1157 | // Determine the time proportion the original bound is along the fling curve 1158 | if (!flingDistance || !flingDuration) { 1159 | timeProportion = 0; 1160 | } else { 1161 | timeProportion = flingBezier._getCoordinateForT(flingBezier.getTForY((flingDistance - beyondBoundsFlingDistance) / flingDistance, 1 / flingDuration), flingBezier._p1.x, flingBezier._p2.x); 1162 | boundsCrossDelay = timeProportion * flingDuration; 1163 | } 1164 | 1165 | // Eighth the distance beyonds the bounds 1166 | modifiedDistance = Math.ceil(beyondBoundsFlingDistance / 8); 1167 | 1168 | // Further limit the bounce to half the container dimensions 1169 | if (Math.abs(modifiedDistance) > _metrics.container[axis] / 2) { 1170 | if (modifiedDistance < 0) { 1171 | modifiedDistance = -Math.floor(_metrics.container[axis] / 2); 1172 | } else { 1173 | modifiedDistance = Math.floor(_metrics.container[axis] / 2); 1174 | } 1175 | } 1176 | 1177 | if (flingPosition > 0) { 1178 | bounceTarget = 0; 1179 | } else { 1180 | bounceTarget = _metrics.scrollEnd[axis]; 1181 | } 1182 | 1183 | // If the entire fling is a bounce, modify appropriately 1184 | if (timeProportion === 0) { 1185 | flingDuration = flingDuration / 6; 1186 | flingPosition = _lastScrollPosition[axis] + baseFlingComponent + modifiedDistance; 1187 | bounceDelay = flingDuration; 1188 | 1189 | // Otherwise, take a new curve and add it to the timeout stack for the bounce 1190 | } else { 1191 | 1192 | // The new bounce delay is the pre-boundary fling duration, plus a 1193 | // sixth of the post-boundary fling. 1194 | bounceDelay = (timeProportion + ((1 - timeProportion) / 6)) * flingDuration; 1195 | 1196 | _scheduleAxisPosition(axis, (_lastScrollPosition[axis] + baseFlingComponent + modifiedDistance), ((1 - timeProportion) * flingDuration / 6), _instanceOptions.bounceDecelerationBezier, boundsCrossDelay); 1197 | 1198 | // Modify the fling to match, clipping to prevent over-fling 1199 | flingBezier = flingBezier.divideAtX(bounceDelay / flingDuration, 1 / flingDuration)[0]; 1200 | flingDuration = bounceDelay; 1201 | flingPosition = (_lastScrollPosition[axis] + baseFlingComponent + modifiedDistance); 1202 | } 1203 | 1204 | // If the fling requires snapping to a snap location, and the bounce needs to 1205 | // reverse the fling direction after the fling completes: 1206 | } else if ((flingDistance < 0 && bounceDistance < flingDistance) || (flingDistance > 0 && bounceDistance > flingDistance)) { 1207 | 1208 | // Shorten the original fling duration to reflect the bounce 1209 | flingPosition = flingPosition - Math.floor(flingDistance / 2); 1210 | bounceDistance = bounceDistance - Math.floor(flingDistance / 2); 1211 | bounceDuration = Math.sqrt(Math.abs(bounceDistance)) * 50; 1212 | bounceTarget = flingPosition - bounceDistance; 1213 | flingDuration = 350; 1214 | bounceDelay = flingDuration * 0.97; 1215 | 1216 | // If the bounce is truncating the fling, or continuing the fling on in the same 1217 | // direction to hit the next boundary: 1218 | } else { 1219 | flingPosition = flingPosition - bounceDistance; 1220 | 1221 | // If there was no fling distance originally, use the bounce details 1222 | if (!flingDistance) { 1223 | flingDuration = bounceDuration; 1224 | 1225 | // If truncating the fling at a snapping edge: 1226 | } else if ((flingDistance < 0 && bounceDistance < 0) || (flingDistance > 0 && bounceDistance > 0)) { 1227 | timeProportion = flingBezier._getCoordinateForT(flingBezier.getTForY((Math.abs(flingDistance) - Math.abs(bounceDistance)) / Math.abs(flingDistance), 1 / flingDuration), flingBezier._p1.x, flingBezier._p2.x); 1228 | flingBezier = flingBezier.divideAtX(timeProportion, 1 / flingDuration)[0]; 1229 | flingDuration = Math.round(flingDuration * timeProportion); 1230 | 1231 | // If extending the fling to reach the next snapping boundary, no further 1232 | // action is required. 1233 | } 1234 | 1235 | bounceDistance = 0; 1236 | bounceDuration = 0; 1237 | } 1238 | } 1239 | 1240 | // If no fling or bounce is required, continue 1241 | if (flingPosition === _lastScrollPosition[axis] && !bounceDistance) { 1242 | continue; 1243 | } 1244 | moveRequired = true; 1245 | 1246 | // Perform the fling 1247 | _setAxisPosition(axis, flingPosition, flingDuration, flingBezier, boundsCrossDelay); 1248 | 1249 | // Schedule a bounce if appropriate 1250 | if (bounceDistance && bounceDuration) { 1251 | _scheduleAxisPosition(axis, bounceTarget, bounceDuration, _instanceOptions.bounceBezier, bounceDelay); 1252 | } 1253 | 1254 | maxAnimationTime = Math.max(maxAnimationTime, bounceDistance ? (bounceDelay + bounceDuration) : flingDuration); 1255 | scrollPositionsToApply[axis] = (bounceTarget === false) ? flingPosition : bounceTarget; 1256 | } 1257 | } 1258 | 1259 | if (moveRequired && maxAnimationTime) { 1260 | _timeouts.push(setTimeout(function () { 1261 | var anAxis; 1262 | 1263 | // Update the stored scroll position ready for finalising 1264 | for (anAxis in scrollPositionsToApply) { 1265 | if (scrollPositionsToApply.hasOwnProperty(anAxis)) { 1266 | _lastScrollPosition[anAxis] = scrollPositionsToApply[anAxis]; 1267 | } 1268 | } 1269 | 1270 | _finalizeScroll(); 1271 | }, maxAnimationTime)); 1272 | } 1273 | 1274 | return moveRequired; 1275 | }; 1276 | 1277 | /** 1278 | * Bounce back into bounds if necessary, or snap to a grid location. 1279 | */ 1280 | _snapScroll = function _snapScroll(scrollCancelled) { 1281 | var axis; 1282 | var snapDuration = scrollCancelled ? 100 : 350; 1283 | var targetPosition = _lastScrollPosition; 1284 | 1285 | // Get the current position and see if a snap is required 1286 | if (_instanceOptions.snapping) { 1287 | 1288 | // Store current snap index 1289 | _snapIndex = _getSnapIndexForPosition(targetPosition); 1290 | targetPosition = _getSnapPositionForIndexes(_snapIndex, targetPosition); 1291 | } 1292 | targetPosition = _limitToBounds(targetPosition); 1293 | 1294 | var snapRequired = false; 1295 | for (axis in _baseScrollableAxes) { 1296 | if (_baseScrollableAxes.hasOwnProperty(axis)) { 1297 | if (targetPosition[axis] !== _lastScrollPosition[axis]) { 1298 | snapRequired = true; 1299 | } 1300 | } 1301 | } 1302 | if (!snapRequired) { 1303 | return false; 1304 | } 1305 | 1306 | // Perform the snap 1307 | for (axis in _baseScrollableAxes) { 1308 | if (_baseScrollableAxes.hasOwnProperty(axis)) { 1309 | _setAxisPosition(axis, targetPosition[axis], snapDuration); 1310 | } 1311 | } 1312 | 1313 | _timeouts.push(setTimeout(function () { 1314 | 1315 | // Update the stored scroll position ready for finalizing 1316 | _lastScrollPosition = targetPosition; 1317 | 1318 | _finalizeScroll(scrollCancelled); 1319 | }, snapDuration)); 1320 | 1321 | return true; 1322 | }; 1323 | 1324 | /** 1325 | * Get an appropriate snap index for a supplied point. 1326 | */ 1327 | _getSnapIndexForPosition = function _getSnapIndexForPosition(coordinates) { 1328 | var axis; 1329 | var indexes = {x: 0, y: 0}; 1330 | for (axis in _scrollableAxes) { 1331 | if (_scrollableAxes.hasOwnProperty(axis) && _snapGridSize[axis]) { 1332 | indexes[axis] = Math.round(coordinates[axis] / _snapGridSize[axis]); 1333 | } 1334 | } 1335 | return indexes; 1336 | }; 1337 | 1338 | /** 1339 | * Get an appropriate snap point for a supplied index. 1340 | */ 1341 | _getSnapPositionForIndexes = function _getSnapPositionForIndexes(indexes, currentCoordinates) { 1342 | var axis; 1343 | var coordinatesToReturn = { 1344 | x: currentCoordinates.x, 1345 | y: currentCoordinates.y 1346 | }; 1347 | for (axis in _scrollableAxes) { 1348 | if (_scrollableAxes.hasOwnProperty(axis)) { 1349 | coordinatesToReturn[axis] = indexes[axis] * _snapGridSize[axis]; 1350 | } 1351 | } 1352 | return coordinatesToReturn; 1353 | }; 1354 | 1355 | /** 1356 | * Update the scroll position while scrolling is active, checking the position 1357 | * within bounds and rubberbanding/constraining as appropriate; also triggers a 1358 | * scroll position render if a requestAnimationFrame loop isn't active 1359 | */ 1360 | _constrainAndRenderTargetScrollPosition = function _constrainAndRenderTargetScrollPosition() { 1361 | var axis, upperBound, lowerBound; 1362 | 1363 | // Update axes target positions if beyond bounds 1364 | for (axis in _scrollableAxes) { 1365 | if (_scrollableAxes.hasOwnProperty(axis)) { 1366 | 1367 | // Set bounds to the left and right of the container 1368 | upperBound = 0; 1369 | lowerBound = _metrics.scrollEnd[axis]; 1370 | 1371 | if (_instanceOptions.singlePageScrolls && _instanceOptions.snapping) { 1372 | 1373 | // For a single-page-scroll, set the bounds to the left and right of the 1374 | // current segment 1375 | upperBound = Math.min(upperBound, -(_baseSegment[axis] - 1) * _snapGridSize[axis]); 1376 | lowerBound = Math.max(lowerBound, -(_baseSegment[axis] + 1) * _snapGridSize[axis]); 1377 | } 1378 | 1379 | if (_targetScrollPosition[axis] > upperBound) { 1380 | _targetScrollPosition[axis] = upperBound + _modifyDistanceBeyondBounds(_targetScrollPosition[axis] - upperBound, axis); 1381 | } else if (_targetScrollPosition[axis] < lowerBound) { 1382 | _targetScrollPosition[axis] = lowerBound + _modifyDistanceBeyondBounds(_targetScrollPosition[axis] - lowerBound, axis); 1383 | } 1384 | } 1385 | } 1386 | 1387 | // Trigger a scroll position update for platforms not using requestAnimationFrames 1388 | if (!_reqAnimationFrame) { 1389 | _scheduleRender(); 1390 | } 1391 | }; 1392 | 1393 | /** 1394 | * Limit coordinates within the bounds of the scrollable viewport. 1395 | */ 1396 | _limitToBounds = function _limitToBounds(coordinates) { 1397 | var axis; 1398 | var coordinatesToReturn = { x: coordinates.x, y: coordinates.y }; 1399 | 1400 | for (axis in _scrollableAxes) { 1401 | if (_scrollableAxes.hasOwnProperty(axis)) { 1402 | 1403 | // If the coordinate is beyond the edges of the scroller, use the closest edge 1404 | if (coordinates[axis] > 0) { 1405 | coordinatesToReturn[axis] = 0; 1406 | continue; 1407 | } 1408 | if (coordinates[axis] < _metrics.scrollEnd[axis]) { 1409 | coordinatesToReturn[axis] = _metrics.scrollEnd[axis]; 1410 | continue; 1411 | } 1412 | } 1413 | } 1414 | 1415 | return coordinatesToReturn; 1416 | }; 1417 | 1418 | 1419 | /** 1420 | * Sets up the DOM around the node to be scrolled. 1421 | */ 1422 | _initializeDOM = function _initializeDOM() { 1423 | var offscreenFragment, offscreenNode, scrollYParent; 1424 | 1425 | // Check whether the DOM is already present and valid - if so, no further action required. 1426 | if (_existingDOMValid()) { 1427 | return; 1428 | } 1429 | 1430 | // Otherwise, the DOM needs to be created inside the originally supplied node. The node 1431 | // has a container inserted inside it - which acts as an anchor element with constraints - 1432 | // and then the scrollable layers as appropriate. 1433 | 1434 | // Create a new document fragment to temporarily hold the scrollable content 1435 | offscreenFragment = _scrollableMasterNode.ownerDocument.createDocumentFragment(); 1436 | offscreenNode = document.createElement('DIV'); 1437 | offscreenFragment.appendChild(offscreenNode); 1438 | 1439 | // Drop in the wrapping HTML 1440 | offscreenNode.innerHTML = FTScroller.prototype.getPrependedHTML(!_instanceOptions.scrollingX, !_instanceOptions.scrollingY, _instanceOptions.hwAccelerationClass) + FTScroller.prototype.getAppendedHTML(!_instanceOptions.scrollingX, !_instanceOptions.scrollingY, _instanceOptions.hwAccelerationClass, _instanceOptions.scrollbars); 1441 | 1442 | // Update references as appropriate 1443 | _containerNode = offscreenNode.firstElementChild; 1444 | scrollYParent = _containerNode; 1445 | if (_instanceOptions.scrollingX) { 1446 | _scrollNodes.x = _containerNode.firstElementChild; 1447 | scrollYParent = _scrollNodes.x; 1448 | if (_instanceOptions.scrollbars) { 1449 | _scrollbarNodes.x = _containerNode.getElementsByClassName('ftscroller_scrollbarx')[0]; 1450 | } 1451 | } 1452 | if (_instanceOptions.scrollingY) { 1453 | _scrollNodes.y = scrollYParent.firstElementChild; 1454 | if (_instanceOptions.scrollbars) { 1455 | _scrollbarNodes.y = _containerNode.getElementsByClassName('ftscroller_scrollbary')[0]; 1456 | } 1457 | _contentParentNode = _scrollNodes.y; 1458 | } else { 1459 | _contentParentNode = _scrollNodes.x; 1460 | } 1461 | 1462 | // Take the contents of the scrollable element, and copy them into the new container 1463 | while (_scrollableMasterNode.firstChild) { 1464 | _contentParentNode.appendChild(_scrollableMasterNode.firstChild); 1465 | } 1466 | 1467 | // Move the wrapped elements back into the document 1468 | _scrollableMasterNode.appendChild(_containerNode); 1469 | }; 1470 | 1471 | /** 1472 | * Attempts to use any existing DOM scroller nodes if possible, returning true if so; 1473 | * updates all internal element references. 1474 | */ 1475 | _existingDOMValid = function _existingDOMValid() { 1476 | var scrollerContainer, layerX, layerY, yParent, scrollerX, scrollerY, candidates, i, l; 1477 | 1478 | // Check that there's an initial child node, and make sure it's the container class 1479 | scrollerContainer = _scrollableMasterNode.firstElementChild; 1480 | if (!scrollerContainer || scrollerContainer.className.indexOf('ftscroller_container') === -1) { 1481 | return; 1482 | } 1483 | 1484 | // If x-axis scrolling is enabled, find and verify the x scroller layer 1485 | if (_instanceOptions.scrollingX) { 1486 | 1487 | // Find and verify the x scroller layer 1488 | layerX = scrollerContainer.firstElementChild; 1489 | if (!layerX || layerX.className.indexOf('ftscroller_x') === -1) { 1490 | return; 1491 | } 1492 | yParent = layerX; 1493 | 1494 | // Find and verify the x scrollbar if enabled 1495 | if (_instanceOptions.scrollbars) { 1496 | candidates = scrollerContainer.getElementsByClassName('ftscroller_scrollbarx'); 1497 | if (candidates) { 1498 | for (i = 0, l = candidates.length; i < l; i = i + 1) { 1499 | if (candidates[i].parentNode === scrollerContainer) { 1500 | scrollerX = candidates[i]; 1501 | break; 1502 | } 1503 | } 1504 | } 1505 | if (!scrollerX) { 1506 | return; 1507 | } 1508 | } 1509 | } else { 1510 | yParent = scrollerContainer; 1511 | } 1512 | 1513 | // If y-axis scrolling is enabled, find and verify the y scroller layer 1514 | if (_instanceOptions.scrollingY) { 1515 | 1516 | // Find and verify the x scroller layer 1517 | layerY = yParent.firstElementChild; 1518 | if (!layerY || layerY.className.indexOf('ftscroller_y') === -1) { 1519 | return; 1520 | } 1521 | 1522 | // Find and verify the y scrollbar if enabled 1523 | if (_instanceOptions.scrollbars) { 1524 | candidates = scrollerContainer.getElementsByClassName('ftscroller_scrollbary'); 1525 | if (candidates) { 1526 | for (i = 0, l = candidates.length; i < l; i = i + 1) { 1527 | if (candidates[i].parentNode === scrollerContainer) { 1528 | scrollerY = candidates[i]; 1529 | break; 1530 | } 1531 | } 1532 | } 1533 | if (!scrollerY) { 1534 | return; 1535 | } 1536 | } 1537 | } 1538 | 1539 | // Elements found and verified - update the references and return success 1540 | _containerNode = scrollerContainer; 1541 | if (layerX) { 1542 | _scrollNodes.x = layerX; 1543 | } 1544 | if (layerY) { 1545 | _scrollNodes.y = layerY; 1546 | } 1547 | if (scrollerX) { 1548 | _scrollbarNodes.x = scrollerX; 1549 | } 1550 | if (scrollerY) { 1551 | _scrollbarNodes.y = scrollerY; 1552 | } 1553 | if (_instanceOptions.scrollingY) { 1554 | _contentParentNode = layerY; 1555 | } else { 1556 | _contentParentNode = layerX; 1557 | } 1558 | return true; 1559 | }; 1560 | 1561 | _domChanged = function _domChanged(e) { 1562 | 1563 | // If the timer is active, clear it 1564 | if (_domChangeDebouncer) { 1565 | window.clearTimeout(_domChangeDebouncer); 1566 | } 1567 | 1568 | // React to resizes at once 1569 | if (e && e.type === 'resize') { 1570 | _updateDimensions(); 1571 | 1572 | // For other changes, which may occur in groups, set up the DOM changed timer 1573 | } else { 1574 | _domChangeDebouncer = setTimeout(function () { 1575 | _updateDimensions(); 1576 | }, 100); 1577 | } 1578 | }; 1579 | 1580 | _updateDimensions = function _updateDimensions(ignoreSnapScroll) { 1581 | var axis; 1582 | 1583 | // Only update dimensions if the container node exists (DOM elements can go away if 1584 | // the scroller instance is not destroyed correctly) 1585 | if (!_containerNode || !_contentParentNode) { 1586 | return false; 1587 | } 1588 | 1589 | if (_domChangeDebouncer) { 1590 | window.clearTimeout(_domChangeDebouncer); 1591 | _domChangeDebouncer = false; 1592 | } 1593 | var containerWidth, containerHeight, startAlignments; 1594 | 1595 | // Calculate the starting alignment for comparison later 1596 | startAlignments = { x: false, y: false }; 1597 | for (axis in startAlignments) { 1598 | if (startAlignments.hasOwnProperty(axis)) { 1599 | if (_lastScrollPosition[axis] === 0) { 1600 | startAlignments[axis] = -1; 1601 | } else if (_lastScrollPosition[axis] <= _metrics.scrollEnd[axis]) { 1602 | startAlignments[axis] = 1; 1603 | } else if (_lastScrollPosition[axis] * 2 <= _metrics.scrollEnd[axis] + 5 && _lastScrollPosition[axis] * 2 >= _metrics.scrollEnd[axis] - 5) { 1604 | startAlignments[axis] = 0; 1605 | } 1606 | } 1607 | } 1608 | 1609 | containerWidth = _containerNode.offsetWidth; 1610 | containerHeight = _containerNode.offsetHeight; 1611 | 1612 | // Grab the dimensions 1613 | var rawScrollWidth = options.contentWidth || _contentParentNode.offsetWidth; 1614 | var rawScrollHeight = options.contentHeight || _contentParentNode.offsetHeight; 1615 | var scrollWidth = rawScrollWidth; 1616 | var scrollHeight = rawScrollHeight; 1617 | var targetPosition = { x: _lastScrollPosition.x, y: _lastScrollPosition.y }; 1618 | 1619 | // Update snap grid 1620 | if (!_snapGridSize.userX) { 1621 | _snapGridSize.x = containerWidth; 1622 | } 1623 | if (!_snapGridSize.userY) { 1624 | _snapGridSize.y = containerHeight; 1625 | } 1626 | 1627 | // If there is a grid, conform to the grid 1628 | if (_instanceOptions.snapping) { 1629 | if (_snapGridSize.userX) { 1630 | scrollWidth = Math.ceil(scrollWidth / _snapGridSize.userX) * _snapGridSize.userX; 1631 | } else { 1632 | scrollWidth = Math.ceil(scrollWidth / _snapGridSize.x) * _snapGridSize.x; 1633 | } 1634 | if (_snapGridSize.userY) { 1635 | scrollHeight = Math.ceil(scrollHeight / _snapGridSize.userY) * _snapGridSize.userY; 1636 | } else { 1637 | scrollHeight = Math.ceil(scrollHeight / _snapGridSize.y) * _snapGridSize.y; 1638 | } 1639 | } 1640 | 1641 | // If no details have changed, return. 1642 | if (_metrics.container.x === containerWidth && _metrics.container.y === containerHeight && _metrics.content.x === scrollWidth && _metrics.content.y === scrollHeight) { 1643 | return; 1644 | } 1645 | 1646 | // Update the sizes 1647 | _metrics.container.x = containerWidth; 1648 | _metrics.container.y = containerHeight; 1649 | _metrics.content.x = scrollWidth; 1650 | _metrics.content.rawX = rawScrollWidth; 1651 | _metrics.content.y = scrollHeight; 1652 | _metrics.content.rawY = rawScrollHeight; 1653 | _metrics.scrollEnd.x = containerWidth - scrollWidth; 1654 | _metrics.scrollEnd.y = containerHeight - scrollHeight; 1655 | 1656 | _updateScrollbarDimensions(); 1657 | 1658 | // If scrolling is in progress, trigger a scroll update 1659 | if (_isScrolling) { 1660 | _lastScrollPosition.x--; 1661 | _lastScrollPosition.y--; 1662 | _constrainAndRenderTargetScrollPosition(); 1663 | 1664 | // If scrolling *isn't* in progress, snap and realign. 1665 | } else { 1666 | if (!ignoreSnapScroll && _instanceOptions.snapping) { 1667 | 1668 | // Ensure bounds are correct 1669 | _updateSegments(); 1670 | targetPosition = _getSnapPositionForIndexes(_snapIndex, _lastScrollPosition); 1671 | } 1672 | 1673 | // Apply base alignment if appropriate 1674 | for (axis in targetPosition) { 1675 | if (targetPosition.hasOwnProperty(axis)) { 1676 | 1677 | // If the container is smaller than the content, determine whether to apply the 1678 | // alignment. This occurs if a scroll has never taken place, or if the position 1679 | // was previously at the correct "end" and can be maintained. 1680 | if (_metrics.container[axis] < _metrics.content[axis]) { 1681 | if (_hasBeenScrolled && _instanceOptions.baseAlignments[axis] !== startAlignments[axis]) { 1682 | continue; 1683 | } 1684 | } 1685 | 1686 | // Apply the alignment 1687 | if (_instanceOptions.baseAlignments[axis] === 1) { 1688 | targetPosition[axis] = _metrics.scrollEnd[axis]; 1689 | } else if (_instanceOptions.baseAlignments[axis] === 0) { 1690 | targetPosition[axis] = Math.floor(_metrics.scrollEnd[axis] / 2); 1691 | } else if (_instanceOptions.baseAlignments[axis] === -1) { 1692 | targetPosition[axis] = 0; 1693 | } 1694 | } 1695 | } 1696 | 1697 | // Limit to bounds 1698 | targetPosition = _limitToBounds(targetPosition); 1699 | 1700 | if (_instanceOptions.scrollingX && targetPosition.x !== _lastScrollPosition.x) { 1701 | _setAxisPosition('x', targetPosition.x, 0); 1702 | _baseScrollPosition.x = targetPosition.x; 1703 | } 1704 | if (_instanceOptions.scrollingY && targetPosition.y !== _lastScrollPosition.y) { 1705 | _setAxisPosition('y', targetPosition.y, 0); 1706 | _baseScrollPosition.y = targetPosition.y; 1707 | } 1708 | } 1709 | }; 1710 | 1711 | _updateScrollbarDimensions = function _updateScrollbarDimensions() { 1712 | 1713 | // Update scrollbar sizes 1714 | if (_instanceOptions.scrollbars) { 1715 | if (_instanceOptions.scrollingX) { 1716 | _scrollbarNodes.x.style.width = Math.max(6, Math.round(_metrics.container.x * (_metrics.container.x / _metrics.content.x) - 4)) + 'px'; 1717 | } 1718 | if (_instanceOptions.scrollingY) { 1719 | _scrollbarNodes.y.style.height = Math.max(6, Math.round(_metrics.container.y * (_metrics.container.y / _metrics.content.y) - 4)) + 'px'; 1720 | } 1721 | } 1722 | 1723 | // Update scroll caches 1724 | _scrollableAxes = {}; 1725 | if (_instanceOptions.scrollingX && (_metrics.content.x > _metrics.container.x || _instanceOptions.alwaysScroll)) { 1726 | _scrollableAxes.x = true; 1727 | } 1728 | if (_instanceOptions.scrollingY && (_metrics.content.y > _metrics.container.y || _instanceOptions.alwaysScroll)) { 1729 | _scrollableAxes.y = true; 1730 | } 1731 | }; 1732 | 1733 | _updateElementPosition = function _updateElementPosition() { 1734 | var axis, computedStyle, splitStyle; 1735 | 1736 | // Retrieve the current position of each active axis. 1737 | // Custom parsing is used instead of native matrix support for speed and for 1738 | // backwards compatibility. 1739 | for (axis in _scrollableAxes) { 1740 | if (_scrollableAxes.hasOwnProperty(axis)) { 1741 | computedStyle = window.getComputedStyle(_scrollNodes[axis], null)[_vendorTransformLookup]; 1742 | splitStyle = computedStyle.split(', '); 1743 | 1744 | // For 2d-style transforms, pull out elements four or five 1745 | if (splitStyle.length === 6) { 1746 | _baseScrollPosition[axis] = parseInt(splitStyle[(axis === 'y') ? 5 : 4], 10); 1747 | 1748 | // For 3d-style transforms, pull out elements twelve or thirteen 1749 | } else { 1750 | _baseScrollPosition[axis] = parseInt(splitStyle[(axis === 'y') ? 13 : 12], 10); 1751 | } 1752 | _lastScrollPosition[axis] = _baseScrollPosition[axis]; 1753 | } 1754 | } 1755 | }; 1756 | 1757 | _updateSegments = function _updateSegments(scrollFinalised) { 1758 | var axis; 1759 | var newSegment = { x: 0, y: 0 }; 1760 | 1761 | // If snapping is disabled, return without any further action required 1762 | if (!_instanceOptions.snapping) { 1763 | return; 1764 | } 1765 | 1766 | // Calculate the new segments 1767 | for (axis in _scrollableAxes) { 1768 | if (_scrollableAxes.hasOwnProperty(axis)) { 1769 | newSegment[axis] = Math.max(0, Math.min(Math.ceil(_metrics.content[axis] / _snapGridSize[axis]) - 1, Math.round(-_lastScrollPosition[axis] / _snapGridSize[axis]))); 1770 | } 1771 | } 1772 | 1773 | // In all cases update the active segment if appropriate 1774 | if (newSegment.x !== _activeSegment.x || newSegment.y !== _activeSegment.y) { 1775 | _activeSegment.x = newSegment.x; 1776 | _activeSegment.y = newSegment.y; 1777 | _fireEvent('segmentwillchange', { segmentX: newSegment.x, segmentY: newSegment.y }); 1778 | } 1779 | 1780 | // If the scroll has been finalised, also update the base segment 1781 | if (scrollFinalised) { 1782 | if (newSegment.x !== _baseSegment.x || newSegment.y !== _baseSegment.y) { 1783 | _baseSegment.x = newSegment.x; 1784 | _baseSegment.y = newSegment.y; 1785 | _fireEvent('segmentdidchange', { segmentX: newSegment.x, segmentY: newSegment.y }); 1786 | } 1787 | } 1788 | }; 1789 | 1790 | _setAxisPosition = function _setAxisPosition(axis, position, animationDuration, animationBezier, boundsCrossDelay) { 1791 | var transitionCSSString, newPositionAtExtremity = null; 1792 | 1793 | // Only update position if the axis node exists (DOM elements can go away if 1794 | // the scroller instance is not destroyed correctly) 1795 | if (!_scrollNodes[axis]) { 1796 | return false; 1797 | } 1798 | 1799 | // Determine the transition property to apply to both the scroll element and the scrollbar 1800 | if (animationDuration) { 1801 | if (!animationBezier) { 1802 | animationBezier = _instanceOptions.flingBezier; 1803 | } 1804 | 1805 | transitionCSSString = _vendorCSSPrefix + 'transform ' + animationDuration + 'ms ' + animationBezier.toString(); 1806 | } else { 1807 | transitionCSSString = ''; 1808 | } 1809 | 1810 | // Apply the transition property to elements 1811 | _scrollNodes[axis].style[_transitionProperty] = transitionCSSString; 1812 | if (_instanceOptions.scrollbars) { 1813 | _scrollbarNodes[axis].style[_transitionProperty] = transitionCSSString; 1814 | } 1815 | 1816 | // Update the positions 1817 | _scrollNodes[axis].style[_transformProperty] = _translateRulePrefix + _transformPrefixes[axis] + position + 'px' + _transformSuffixes[axis]; 1818 | if (_instanceOptions.scrollbars) { 1819 | _scrollbarNodes[axis].style[_transformProperty] = _translateRulePrefix + _transformPrefixes[axis] + (-position * _metrics.container[axis] / _metrics.content[axis]) + 'px' + _transformSuffixes[axis]; 1820 | } 1821 | 1822 | // Determine whether the scroll is at an extremity. 1823 | if (position >= 0) { 1824 | newPositionAtExtremity = 'start'; 1825 | } else if (position <= _metrics.scrollEnd[axis]) { 1826 | newPositionAtExtremity = 'end'; 1827 | } 1828 | 1829 | // If the extremity status has changed, fire an appropriate event 1830 | if (newPositionAtExtremity !== _scrollAtExtremity[axis]) { 1831 | if (newPositionAtExtremity !== null) { 1832 | if (animationDuration) { 1833 | _timeouts.push(setTimeout(function() { 1834 | _fireEvent('reached' + newPositionAtExtremity, { axis: axis }); 1835 | }, boundsCrossDelay || animationDuration)); 1836 | } else { 1837 | _fireEvent('reached' + newPositionAtExtremity, { axis: axis }); 1838 | } 1839 | } 1840 | _scrollAtExtremity[axis] = newPositionAtExtremity; 1841 | } 1842 | 1843 | // Update the recorded position if there's no duration 1844 | if (!animationDuration) { 1845 | _lastScrollPosition[axis] = position; 1846 | } 1847 | }; 1848 | 1849 | /** 1850 | * Retrieve the current position as an object with scrollLeft and scrollTop 1851 | * properties. 1852 | */ 1853 | _getPosition = function _getPosition() { 1854 | return { 1855 | scrollLeft: -_lastScrollPosition.x, 1856 | scrollTop: -_lastScrollPosition.y 1857 | }; 1858 | }; 1859 | 1860 | _scheduleAxisPosition = function _scheduleAxisPosition(axis, position, animationDuration, animationBezier, afterDelay) { 1861 | _timeouts.push(setTimeout(function () { 1862 | _setAxisPosition(axis, position, animationDuration, animationBezier); 1863 | }, afterDelay)); 1864 | }; 1865 | 1866 | _fireEvent = function _fireEvent(eventName, eventObject) { 1867 | var i, l; 1868 | eventObject.srcObject = _publicSelf; 1869 | 1870 | // Iterate through any listeners 1871 | for (i = 0, l = _eventListeners[eventName].length; i < l; i = i + 1) { 1872 | 1873 | // Execute each in a try/catch 1874 | try { 1875 | _eventListeners[eventName][i](eventObject); 1876 | } catch (error) { 1877 | if (window.console && window.console.error) { 1878 | if (error.message) { 1879 | window.console.error(error.message + ' (' + error.sourceURL + ', line ' + error.line + ')'); 1880 | } else { 1881 | window.console.error('Error encountered executing FTScroller event listener callback for [' + eventName + ']. Add a "debugger" statement here to obtain a full backtrace.'); 1882 | if (window.console.dir) window.console.dir(error); 1883 | } 1884 | } 1885 | } 1886 | } 1887 | }; 1888 | 1889 | /** 1890 | * Update the scroll position so that the child element is in view. 1891 | */ 1892 | _childFocused = function _childFocused(event) { 1893 | var offset, axis, visibleChildPortion; 1894 | var focusedNodeRect = _getBoundingRect(event.target); 1895 | var containerRect = _getBoundingRect(_containerNode); 1896 | var edgeMap = { x: 'left', y: 'top' }; 1897 | var opEdgeMap = { x: 'right', y: 'bottom' }; 1898 | var dimensionMap = { x: 'width', y: 'height' }; 1899 | 1900 | // If an input is currently being tracked, ignore the focus event 1901 | if (_inputIdentifier !== false) { 1902 | return; 1903 | } 1904 | 1905 | for (axis in _scrollableAxes) { 1906 | if (_scrollableAxes.hasOwnProperty(axis)) { 1907 | 1908 | // If the focussed node is entirely in view, there is no need to center it 1909 | if (focusedNodeRect[edgeMap[axis]] >= containerRect[edgeMap[axis]] && focusedNodeRect[opEdgeMap[axis]] <= containerRect[opEdgeMap[axis]]) { 1910 | continue; 1911 | } 1912 | 1913 | // If the focussed node is larger than the container... 1914 | if (focusedNodeRect[dimensionMap[axis]] > containerRect[dimensionMap[axis]]) { 1915 | 1916 | visibleChildPortion = focusedNodeRect[dimensionMap[axis]] - Math.max(0, containerRect[edgeMap[axis]] - focusedNodeRect[edgeMap[axis]]) - Math.max(0, focusedNodeRect[opEdgeMap[axis]] - containerRect[opEdgeMap[axis]]); 1917 | 1918 | // If more than half a container's portion of the focussed node is visible, there's no need to center it 1919 | if (visibleChildPortion >= (containerRect[dimensionMap[axis]] / 2)) { 1920 | continue; 1921 | } 1922 | } 1923 | 1924 | // Set the target offset to be in the middle of the container, or as close as bounds permit 1925 | offset = -Math.round((focusedNodeRect[dimensionMap[axis]] / 2) - _lastScrollPosition[axis] + focusedNodeRect[edgeMap[axis]] - containerRect[edgeMap[axis]] - (containerRect[dimensionMap[axis]] / 2)); 1926 | offset = Math.min(0, Math.max(_metrics.scrollEnd[axis], offset)); 1927 | 1928 | // Perform the scroll 1929 | _setAxisPosition(axis, offset, 0); 1930 | _baseScrollPosition[axis] = offset; 1931 | } 1932 | } 1933 | 1934 | _fireEvent('scroll', _getPosition()); 1935 | }; 1936 | 1937 | /** 1938 | * Given a relative distance beyond the element bounds, returns a modified version to 1939 | * simulate bouncy/springy edges. 1940 | */ 1941 | _modifyDistanceBeyondBounds = function _modifyDistanceBeyondBounds(distance, axis) { 1942 | if (!_instanceOptions.bouncing) { 1943 | return 0; 1944 | } 1945 | var e = Math.exp(distance / _metrics.container[axis]); 1946 | return Math.round(_metrics.container[axis] * 0.6 * (e - 1) / (e + 1)); 1947 | }; 1948 | 1949 | /** 1950 | * Given positions for each enabled axis, returns an object showing how far each axis is beyond 1951 | * bounds. If within bounds, -1 is returned; if at the bounds, 0 is returned. 1952 | */ 1953 | _distancesBeyondBounds = function _distancesBeyondBounds(positions) { 1954 | var axis, position; 1955 | var distances = {}; 1956 | for (axis in positions) { 1957 | if (positions.hasOwnProperty(axis)) { 1958 | position = positions[axis]; 1959 | 1960 | // If the position is to the left/top, no further modification required 1961 | if (position >= 0) { 1962 | distances[axis] = position; 1963 | 1964 | // If it's within the bounds, use -1 1965 | } else if (position > _metrics.scrollEnd[axis]) { 1966 | distances[axis] = -1; 1967 | 1968 | // Otherwise, amend by the distance of the maximum edge 1969 | } else { 1970 | distances[axis] = _metrics.scrollEnd[axis] - position; 1971 | } 1972 | } 1973 | } 1974 | return distances; 1975 | }; 1976 | 1977 | /** 1978 | * On platforms which support it, use RequestAnimationFrame to group 1979 | * position updates for speed. Starts the render process. 1980 | */ 1981 | _startAnimation = function _startAnimation() { 1982 | if (_reqAnimationFrame) { 1983 | _cancelAnimation(); 1984 | _animationFrameRequest = _reqAnimationFrame(_scheduleRender); 1985 | } 1986 | }; 1987 | 1988 | /** 1989 | * On platforms which support RequestAnimationFrame, provide the rendering loop. 1990 | * Takes two arguments; the first is the render/position update function to 1991 | * be called, and the second is a string controlling the render type to 1992 | * allow previous changes to be cancelled - should be 'pan' or 'scroll'. 1993 | */ 1994 | _scheduleRender = function _scheduleRender() { 1995 | var axis, positionUpdated; 1996 | 1997 | // If using requestAnimationFrame schedule the next update at once 1998 | if (_reqAnimationFrame) { 1999 | _animationFrameRequest = _reqAnimationFrame(_scheduleRender); 2000 | } 2001 | 2002 | // Perform the draw. 2003 | for (axis in _scrollableAxes) { 2004 | if (_scrollableAxes.hasOwnProperty(axis) && _targetScrollPosition[axis] !== _lastScrollPosition[axis]) { 2005 | _setAxisPosition(axis, _targetScrollPosition[axis]); 2006 | positionUpdated = true; 2007 | } 2008 | } 2009 | 2010 | // If full, locked scrolling has enabled, fire any scroll and segment change events 2011 | if (_isScrolling && positionUpdated) { 2012 | _fireEvent('scroll', _getPosition()); 2013 | _updateSegments(false); 2014 | } 2015 | }; 2016 | 2017 | /** 2018 | * Stops the animation process. 2019 | */ 2020 | _cancelAnimation = function _cancelAnimation() { 2021 | if (_animationFrameRequest === false || !_cancelAnimationFrame) { 2022 | return; 2023 | } 2024 | 2025 | _cancelAnimationFrame(_animationFrameRequest); 2026 | _animationFrameRequest = false; 2027 | }; 2028 | 2029 | /** 2030 | * Remove then re-set event handlers 2031 | */ 2032 | _resetEventHandlers = function() { 2033 | _removeEventHandlers(); 2034 | _addEventHandlers(); 2035 | }; 2036 | 2037 | /** 2038 | * Register event handlers 2039 | */ 2040 | _addEventHandlers = function _addEventHandlers() { 2041 | var MutationObserver; 2042 | 2043 | // Only remove the event if the node exists (DOM elements can go away) 2044 | if (!_containerNode) { 2045 | return; 2046 | } 2047 | 2048 | if (_trackPointerEvents && !_instanceOptions.disabledInputMethods.pointer) { 2049 | if (_pointerEventsPrefixed) { 2050 | _containerNode.addEventListener('MSPointerDown', _onPointerDown); 2051 | _containerNode.addEventListener('MSPointerMove', _onPointerMove); 2052 | _containerNode.addEventListener('MSPointerUp', _onPointerUp); 2053 | _containerNode.addEventListener('MSPointerCancel', _onPointerCancel); 2054 | } else { 2055 | _containerNode.addEventListener('pointerdown', _onPointerDown); 2056 | _containerNode.addEventListener('pointermove', _onPointerMove); 2057 | _containerNode.addEventListener('pointerup', _onPointerUp); 2058 | _containerNode.addEventListener('pointercancel', _onPointerCancel); 2059 | } 2060 | } else { 2061 | if (_trackTouchEvents && !_instanceOptions.disabledInputMethods.touch) { 2062 | _containerNode.addEventListener('touchstart', _onTouchStart); 2063 | _containerNode.addEventListener('touchmove', _onTouchMove); 2064 | _containerNode.addEventListener('touchend', _onTouchEnd); 2065 | _containerNode.addEventListener('touchcancel', _onTouchEnd); 2066 | } 2067 | if (!_instanceOptions.disabledInputMethods.mouse) { 2068 | _containerNode.addEventListener('mousedown', _onMouseDown); 2069 | } 2070 | } 2071 | if (!_instanceOptions.disabledInputMethods.scroll) { 2072 | _containerNode.addEventListener('DOMMouseScroll', _onMouseScroll); 2073 | _containerNode.addEventListener('mousewheel', _onMouseScroll); 2074 | } 2075 | 2076 | // If any of the input methods which would eventually trigger a click are 2077 | // enabled, add a click event listener so that phantom clicks can be prevented 2078 | // at the end of a scroll. Otherwise, don't add a listener and don't prevent 2079 | // clicks. 2080 | if (!_instanceOptions.disabledInputMethods.mouse || !_instanceOptions.disabledInputMethods.touch || !_instanceOptions.disabledInputMethods.pointer) { 2081 | 2082 | // Add a click listener. On IE, add the listener to the document, to allow 2083 | // clicks to be cancelled if a scroll ends outside the bounds of the container; on 2084 | // other platforms, add to the container node. 2085 | if (_trackPointerEvents) { 2086 | document.addEventListener('click', _onClick, true); 2087 | } else { 2088 | _containerNode.addEventListener('click', _onClick, true); 2089 | } 2090 | } 2091 | 2092 | // Watch for changes inside the contained element to update bounds - de-bounced slightly. 2093 | if (!_instanceOptions.disabledInputMethods.focus) { 2094 | _contentParentNode.addEventListener('focus', _childFocused); 2095 | } 2096 | if (_instanceOptions.updateOnChanges) { 2097 | 2098 | // Try and reuse the old, disconnected observer instance if available 2099 | // Otherwise, check for support before proceeding 2100 | if (!_mutationObserver) { 2101 | MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window[_vendorStylePropertyPrefix + 'MutationObserver']; 2102 | if (MutationObserver) { 2103 | _mutationObserver = new MutationObserver(_domChanged); 2104 | } 2105 | } 2106 | 2107 | if (_mutationObserver) { 2108 | _mutationObserver.observe(_contentParentNode, { 2109 | childList: true, 2110 | characterData: true, 2111 | subtree: true 2112 | }); 2113 | } else { 2114 | _contentParentNode.addEventListener('DOMSubtreeModified', function (e) { 2115 | 2116 | // Ignore changes to nested FT Scrollers - even updating a transform style 2117 | // can trigger a DOMSubtreeModified in IE, causing nested scrollers to always 2118 | // favour the deepest scroller as parent scrollers 'resize'/end scrolling. 2119 | if (e && (e.srcElement === _contentParentNode || e.srcElement.className.indexOf('ftscroller_') !== -1)) { 2120 | return; 2121 | } 2122 | 2123 | _domChanged(); 2124 | }, true); 2125 | } 2126 | _contentParentNode.addEventListener('load', _domChanged); 2127 | } 2128 | if (_instanceOptions.updateOnWindowResize) { 2129 | window.addEventListener('resize', _domChanged); 2130 | } 2131 | }; 2132 | 2133 | /** 2134 | * Remove event handlers. 2135 | * 2136 | * The current flags may not match the state when the event handlers were set up, 2137 | * so remove all event handlers unconditionally, just in case they're bound. 2138 | */ 2139 | _removeEventHandlers = function _removeEventHandlers() { 2140 | 2141 | if (_containerNode) { 2142 | _containerNode.removeEventListener('MSPointerDown', _onPointerDown); 2143 | _containerNode.removeEventListener('MSPointerMove', _onPointerMove); 2144 | _containerNode.removeEventListener('MSPointerUp', _onPointerUp); 2145 | _containerNode.removeEventListener('MSPointerCancel', _onPointerCancel); 2146 | _containerNode.removeEventListener('pointerdown', _onPointerDown); 2147 | _containerNode.removeEventListener('pointermove', _onPointerMove); 2148 | _containerNode.removeEventListener('pointerup', _onPointerUp); 2149 | _containerNode.removeEventListener('pointercancel', _onPointerCancel); 2150 | _containerNode.removeEventListener('touchstart', _onTouchStart); 2151 | _containerNode.removeEventListener('touchmove', _onTouchMove); 2152 | _containerNode.removeEventListener('touchend', _onTouchEnd); 2153 | _containerNode.removeEventListener('touchcancel', _onTouchEnd); 2154 | _containerNode.removeEventListener('mousedown', _onMouseDown); 2155 | _containerNode.removeEventListener('DOMMouseScroll', _onMouseScroll); 2156 | _containerNode.removeEventListener('mousewheel', _onMouseScroll); 2157 | _containerNode.removeEventListener('click', _onClick, true); 2158 | } 2159 | 2160 | if (_contentParentNode) { 2161 | _contentParentNode.removeEventListener('focus', _childFocused); 2162 | _contentParentNode.removeEventListener('DOMSubtreeModified', _domChanged); 2163 | _contentParentNode.removeEventListener('load', _domChanged); 2164 | } 2165 | 2166 | if (_mutationObserver) { 2167 | _mutationObserver.disconnect(); 2168 | } 2169 | 2170 | document.removeEventListener('mousemove', _onMouseMove); 2171 | document.removeEventListener('mouseup', _onMouseUp); 2172 | document.removeEventListener('click', _onClick, true); 2173 | window.removeEventListener('resize', _domChanged); 2174 | }; 2175 | 2176 | /** 2177 | * Touch event handlers 2178 | */ 2179 | _onTouchStart = function _onTouchStart(startEvent) { 2180 | var i, l, touchEvent; 2181 | 2182 | // If a touch is already active, ensure that the index 2183 | // is mapped to the correct finger, and return. 2184 | if (_inputIdentifier) { 2185 | for (i = 0, l = startEvent.touches.length; i < l; i = i + 1) { 2186 | if (startEvent.touches[i].identifier === _inputIdentifier) { 2187 | _inputIndex = i; 2188 | } 2189 | } 2190 | return; 2191 | } 2192 | 2193 | // Track the new touch's identifier, reset index, and pass 2194 | // the coordinates to the scroll start function. 2195 | touchEvent = startEvent.touches[0]; 2196 | _inputIdentifier = touchEvent.identifier; 2197 | _inputIndex = 0; 2198 | _startScroll(touchEvent.clientX, touchEvent.clientY, startEvent.timeStamp, startEvent); 2199 | }; 2200 | _onTouchMove = function _onTouchMove(moveEvent) { 2201 | if (_inputIdentifier === false) { 2202 | return; 2203 | } 2204 | 2205 | // Get the coordinates from the appropriate touch event and 2206 | // pass them on to the scroll handler 2207 | var touchEvent = moveEvent.touches[_inputIndex]; 2208 | _updateScroll(touchEvent.clientX, touchEvent.clientY, moveEvent.timeStamp, moveEvent); 2209 | }; 2210 | _onTouchEnd = function _onTouchEnd(endEvent) { 2211 | var i, l; 2212 | 2213 | // Check whether the original touch event is still active, 2214 | // if it is, update the index and return. 2215 | if (endEvent.touches) { 2216 | for (i = 0, l = endEvent.touches.length; i < l; i = i + 1) { 2217 | if (endEvent.touches[i].identifier === _inputIdentifier) { 2218 | _inputIndex = i; 2219 | return; 2220 | } 2221 | } 2222 | } 2223 | 2224 | // Complete the scroll. Note that touch end events 2225 | // don't capture coordinates. 2226 | _endScroll(endEvent.timeStamp, endEvent); 2227 | }; 2228 | 2229 | /** 2230 | * Mouse event handlers 2231 | */ 2232 | _onMouseDown = function _onMouseDown(startEvent) { 2233 | 2234 | // Don't track the right mouse buttons, or a context menu 2235 | if ((startEvent.button && startEvent.button === 2) || startEvent.ctrlKey) { 2236 | return; 2237 | } 2238 | 2239 | // Capture if possible 2240 | if (_containerNode.setCapture) { 2241 | _containerNode.setCapture(); 2242 | } 2243 | 2244 | // Add move & up handlers to the *document* to allow handling outside the element 2245 | document.addEventListener('mousemove', _onMouseMove, true); 2246 | document.addEventListener('mouseup', _onMouseUp, true); 2247 | 2248 | _inputIdentifier = startEvent.button || 1; 2249 | _inputIndex = 0; 2250 | _startScroll(startEvent.clientX, startEvent.clientY, startEvent.timeStamp, startEvent); 2251 | }; 2252 | _onMouseMove = function _onMouseMove(moveEvent) { 2253 | if (!_inputIdentifier) { 2254 | return; 2255 | } 2256 | 2257 | _updateScroll(moveEvent.clientX, moveEvent.clientY, moveEvent.timeStamp, moveEvent); 2258 | }; 2259 | _onMouseUp = function _onMouseUp(endEvent) { 2260 | if (endEvent.button && endEvent.button !== _inputIdentifier) { 2261 | return; 2262 | } 2263 | 2264 | document.removeEventListener('mousemove', _onMouseMove, true); 2265 | document.removeEventListener('mouseup', _onMouseUp, true); 2266 | 2267 | // Release capture if possible 2268 | if (_containerNode.releaseCapture) { 2269 | _containerNode.releaseCapture(); 2270 | } 2271 | 2272 | _endScroll(endEvent.timeStamp, endEvent); 2273 | }; 2274 | 2275 | /** 2276 | * Pointer event handlers 2277 | */ 2278 | _onPointerDown = function _onPointerDown(startEvent) { 2279 | 2280 | // If there is already a pointer event being tracked, ignore subsequent. 2281 | // However, if this pointer is seen as the primary pointer, override that. 2282 | if (_inputIdentifier && !startEvent.isPrimary) { 2283 | return; 2284 | } 2285 | 2286 | // Disable specific input types if specified in the config. Separate 2287 | // out touch and other events (eg treat both pen and mouse as "mouse") 2288 | if (startEvent.pointerType === _pointerTypeTouch) { 2289 | if (_instanceOptions.disabledInputMethods.touch) { 2290 | return; 2291 | } 2292 | } else if (_instanceOptions.disabledInputMethods.mouse) { 2293 | return; 2294 | } 2295 | 2296 | _inputIdentifier = startEvent.pointerId; 2297 | _startScroll(startEvent.clientX, startEvent.clientY, startEvent.timeStamp, startEvent); 2298 | }; 2299 | _onPointerMove = function _onPointerMove(moveEvent) { 2300 | if (_inputIdentifier !== moveEvent.pointerId) { 2301 | return; 2302 | } 2303 | _updateScroll(moveEvent.clientX, moveEvent.clientY, moveEvent.timeStamp, moveEvent); 2304 | }; 2305 | _onPointerUp = function _onPointerUp(endEvent) { 2306 | if (_inputIdentifier !== endEvent.pointerId) { 2307 | return; 2308 | } 2309 | 2310 | _endScroll(endEvent.timeStamp, endEvent); 2311 | }; 2312 | _onPointerCancel = function _onPointerCancel(endEvent) { 2313 | _endScroll(endEvent.timeStamp, endEvent); 2314 | }; 2315 | _onPointerCaptureEnd = function _onPointerCaptureEnd(event) { 2316 | 2317 | // On pointer capture end - which can happen because of another element 2318 | // releasing pointer capture - don't end scrolling, but do track that 2319 | // input capture has been lost. This will result in pointers leaving 2320 | // the window possibly being lost, but further interactions will fix 2321 | // the tracking again. 2322 | _inputCaptured = false; 2323 | }; 2324 | 2325 | 2326 | /** 2327 | * Prevents click actions if appropriate 2328 | */ 2329 | _onClick = function _onClick(clickEvent) { 2330 | 2331 | // If a scroll action hasn't resulted in the next scroll being prevented, and a scroll 2332 | // isn't currently in progress with a different identifier, allow the click 2333 | if (!_preventClick) { 2334 | return true; 2335 | } 2336 | 2337 | // Prevent clicks using the preventDefault() and stopPropagation() handlers on the event; 2338 | // this is safe even in IE10 as this is always a "true" event, never a window.event. 2339 | clickEvent.preventDefault(); 2340 | clickEvent.stopPropagation(); 2341 | if (!_inputIdentifier) { 2342 | _preventClick = false; 2343 | } 2344 | return false; 2345 | }; 2346 | 2347 | 2348 | /** 2349 | * Process scroll wheel/input actions as scroller scrolls 2350 | */ 2351 | _onMouseScroll = function _onMouseScroll(event) { 2352 | var scrollDeltaX, scrollDeltaY; 2353 | if (_inputIdentifier !== 'scrollwheel') { 2354 | if (_inputIdentifier !== false) { 2355 | return true; 2356 | } 2357 | _inputIdentifier = 'scrollwheel'; 2358 | _cumulativeScroll.x = 0; 2359 | _cumulativeScroll.y = 0; 2360 | 2361 | // Start a scroll event 2362 | if (!_startScroll(event.clientX, event.clientY, Date.now(), event)) { 2363 | return; 2364 | } 2365 | } 2366 | 2367 | // Convert the scrollwheel values to a scroll value 2368 | if (event.wheelDelta) { 2369 | if (event.wheelDeltaX) { 2370 | scrollDeltaX = event.wheelDeltaX / 2; 2371 | scrollDeltaY = event.wheelDeltaY / 2; 2372 | } else { 2373 | scrollDeltaX = 0; 2374 | scrollDeltaY = event.wheelDelta / 2; 2375 | } 2376 | } else { 2377 | if (event.axis && event.axis === event.HORIZONTAL_AXIS) { 2378 | scrollDeltaX = event.detail * -10; 2379 | scrollDeltaY = 0; 2380 | } else { 2381 | scrollDeltaX = 0; 2382 | scrollDeltaY = event.detail * -10; 2383 | } 2384 | } 2385 | 2386 | // if only one axis scroll is used, bubble up the other scroll wheel event 2387 | // e.g. if only x axis scroll is used then scrolling y axis can bubble up to scroll the page 2388 | if(!_instanceOptions.invertScrollWheel && ( 2389 | (_instanceOptions.scrollingX && !_instanceOptions.scrollingY && Math.abs(scrollDeltaX) < Math.abs(scrollDeltaY)) || 2390 | (!_instanceOptions.scrollingX && _instanceOptions.scrollingY && Math.abs(scrollDeltaX) > Math.abs(scrollDeltaY)) 2391 | )) { 2392 | return; 2393 | } 2394 | 2395 | // If the scroller is constrained to an x axis, convert y scroll to allow single-axis scroll 2396 | // wheels to scroll constrained content. 2397 | if (_instanceOptions.invertScrollWheel && !_instanceOptions.scrollingY && !scrollDeltaX) { 2398 | scrollDeltaX = scrollDeltaY; 2399 | scrollDeltaY = 0; 2400 | } 2401 | 2402 | _cumulativeScroll.x = Math.round(_cumulativeScroll.x + scrollDeltaX); 2403 | _cumulativeScroll.y = Math.round(_cumulativeScroll.y + scrollDeltaY); 2404 | 2405 | _updateScroll(_gestureStart.x + _cumulativeScroll.x, _gestureStart.y + _cumulativeScroll.y, event.timeStamp, event); 2406 | 2407 | // End scrolling state 2408 | if (_scrollWheelEndDebouncer) { 2409 | clearTimeout(_scrollWheelEndDebouncer); 2410 | } 2411 | _scrollWheelEndDebouncer = setTimeout(function () { 2412 | _releaseInputCapture(); 2413 | _inputIdentifier = false; 2414 | _isScrolling = false; 2415 | _preventClick = false; 2416 | _isDisplayingScroll = false; 2417 | _ftscrollerMoving = false; 2418 | if (_instanceOptions.windowScrollingActiveFlag) { 2419 | window[_instanceOptions.windowScrollingActiveFlag] = false; 2420 | } 2421 | _cancelAnimation(); 2422 | if (!_snapScroll()) { 2423 | _finalizeScroll(); 2424 | } 2425 | }, 300); 2426 | }; 2427 | 2428 | /** 2429 | * Capture and release input support, particularly allowing tracking 2430 | * of Metro pointers outside the docked view. Note that _releaseInputCapture 2431 | * should be called before the input identifier is cleared. 2432 | */ 2433 | _captureInput = function _captureInput() { 2434 | if (_inputCaptured || _inputIdentifier === false || _inputIdentifier === 'scrollwheel') { 2435 | return; 2436 | } 2437 | if (_trackPointerEvents) { 2438 | _containerNode[_setPointerCapture](_inputIdentifier); 2439 | _containerNode.addEventListener(_lostPointerCapture, _onPointerCaptureEnd, false); 2440 | } 2441 | _inputCaptured = true; 2442 | }; 2443 | _releaseInputCapture = function _releaseInputCapture() { 2444 | if (!_inputCaptured) { 2445 | return; 2446 | } 2447 | if (_trackPointerEvents) { 2448 | _containerNode.removeEventListener(_lostPointerCapture, _onPointerCaptureEnd, false); 2449 | _containerNode[_releasePointerCapture](_inputIdentifier); 2450 | } 2451 | _inputCaptured = false; 2452 | }; 2453 | 2454 | /** 2455 | * Utility function acting as a getBoundingClientRect polyfill. 2456 | */ 2457 | _getBoundingRect = function _getBoundingRect(anElement) { 2458 | if (anElement.getBoundingClientRect) { 2459 | return anElement.getBoundingClientRect(); 2460 | } 2461 | 2462 | var x = 0, y = 0, eachElement = anElement; 2463 | while (eachElement) { 2464 | x = x + eachElement.offsetLeft - eachElement.scrollLeft; 2465 | y = y + eachElement.offsetTop - eachElement.scrollTop; 2466 | eachElement = eachElement.offsetParent; 2467 | } 2468 | return { left: x, top: y, width: anElement.offsetWidth, height: anElement.offsetHeight }; 2469 | }; 2470 | 2471 | 2472 | /* Instantiation */ 2473 | 2474 | // Set up the DOM node if appropriate 2475 | _initializeDOM(); 2476 | 2477 | // Update sizes 2478 | _updateDimensions(); 2479 | 2480 | // Set up the event handlers 2481 | _addEventHandlers(); 2482 | 2483 | // Define a public API to be returned at the bottom - this is the public-facing interface. 2484 | _publicSelf = { 2485 | destroy: destroy, 2486 | setSnapSize: setSnapSize, 2487 | scrollTo: scrollTo, 2488 | scrollBy: scrollBy, 2489 | updateDimensions: updateDimensions, 2490 | addEventListener: addEventListener, 2491 | removeEventListener: removeEventListener, 2492 | setDisabledInputMethods: setDisabledInputMethods 2493 | }; 2494 | 2495 | if (Object.defineProperties) { 2496 | Object.defineProperties(_publicSelf, { 2497 | 'scrollHeight': { 2498 | get: function() { return _metrics.content.y; }, 2499 | set: function(value) { throw new SyntaxError('scrollHeight is currently read-only - ignoring ' + value); } 2500 | }, 2501 | 'scrollLeft': { 2502 | get: function() { return -_lastScrollPosition.x; }, 2503 | set: function(value) { scrollTo(value, false, false); return -_lastScrollPosition.x; } 2504 | }, 2505 | 'scrollTop': { 2506 | get: function() { return -_lastScrollPosition.y; }, 2507 | set: function(value) { scrollTo(false, value, false); return -_lastScrollPosition.y; } 2508 | }, 2509 | 'scrollWidth': { 2510 | get: function() { return _metrics.content.x; }, 2511 | set: function(value) { throw new SyntaxError('scrollWidth is currently read-only - ignoring ' + value); } 2512 | }, 2513 | 'segmentCount': { 2514 | get: function() { 2515 | if (!_instanceOptions.snapping) { 2516 | return { x: NaN, y: NaN }; 2517 | } 2518 | return { 2519 | x: Math.ceil(_metrics.content.x / _snapGridSize.x), 2520 | y: Math.ceil(_metrics.content.y / _snapGridSize.y) 2521 | }; 2522 | }, 2523 | set: function(value) { throw new SyntaxError('segmentCount is currently read-only - ignoring ' + value); } 2524 | }, 2525 | 'currentSegment': { 2526 | get: function() { return { x: _activeSegment.x, y: _activeSegment.y }; }, 2527 | set: function(value) { throw new SyntaxError('currentSegment is currently read-only - ignoring ' + value); } 2528 | }, 2529 | 'contentContainerNode': { 2530 | get: function() { return _contentParentNode; }, 2531 | set: function(value) { throw new SyntaxError('contentContainerNode is currently read-only - ignoring ' + value); } 2532 | } 2533 | }); 2534 | } 2535 | 2536 | // Return the public interface. 2537 | return _publicSelf; 2538 | }; 2539 | 2540 | 2541 | /* Prototype Functions and Properties */ 2542 | 2543 | /** 2544 | * The HTML to prepend to the scrollable content to wrap it. Used internally, 2545 | * and may be used to pre-wrap scrollable content. Axes can optionally 2546 | * be excluded for speed improvements. 2547 | */ 2548 | FTScroller.prototype.getPrependedHTML = function (excludeXAxis, excludeYAxis, hwAccelerationClass) { 2549 | if (!hwAccelerationClass) { 2550 | if (typeof FTScrollerOptions === 'object' && FTScrollerOptions.hwAccelerationClass) { 2551 | hwAccelerationClass = FTScrollerOptions.hwAccelerationClass; 2552 | } else { 2553 | hwAccelerationClass = 'ftscroller_hwaccelerated'; 2554 | } 2555 | } 2556 | 2557 | var output = '
'; 2558 | if (!excludeXAxis) { 2559 | output += '
'; 2560 | } 2561 | if (!excludeYAxis) { 2562 | output += '
'; 2563 | } 2564 | 2565 | return output; 2566 | }; 2567 | 2568 | /** 2569 | * The HTML to append to the scrollable content to wrap it; again, used internally, 2570 | * and may be used to pre-wrap scrollable content. 2571 | */ 2572 | FTScroller.prototype.getAppendedHTML = function (excludeXAxis, excludeYAxis, hwAccelerationClass, scrollbars) { 2573 | if (!hwAccelerationClass) { 2574 | if (typeof FTScrollerOptions === 'object' && FTScrollerOptions.hwAccelerationClass) { 2575 | hwAccelerationClass = FTScrollerOptions.hwAccelerationClass; 2576 | } else { 2577 | hwAccelerationClass = 'ftscroller_hwaccelerated'; 2578 | } 2579 | } 2580 | 2581 | var output = ''; 2582 | if (!excludeXAxis) { 2583 | output += '
'; 2584 | } 2585 | if (!excludeYAxis) { 2586 | output += '
'; 2587 | } 2588 | if (scrollbars) { 2589 | if (!excludeXAxis) { 2590 | output += '
'; 2591 | } 2592 | if (!excludeYAxis) { 2593 | output += '
'; 2594 | } 2595 | } 2596 | output += '
'; 2597 | 2598 | return output; 2599 | }; 2600 | }()); 2601 | 2602 | 2603 | (function () { 2604 | 'use strict'; 2605 | 2606 | function clamp(value) { 2607 | if (value > 1.0) return 1.0; 2608 | if (value < 0.0) return 0.0; 2609 | return value; 2610 | } 2611 | 2612 | /** 2613 | * Represents a two-dimensional cubic bezier curve with the starting 2614 | * point (0, 0) and the end point (1, 1). The two control points p1 and p2 2615 | * have x and y coordinates between 0 and 1. 2616 | * 2617 | * This type of bezier curves can be used as CSS transform timing functions. 2618 | */ 2619 | CubicBezier = function (p1x, p1y, p2x, p2y) { 2620 | // Control points 2621 | this._p1 = { x: clamp(p1x), y: clamp(p1y) }; 2622 | this._p2 = { x: clamp(p2x), y: clamp(p2y) }; 2623 | }; 2624 | 2625 | CubicBezier.prototype._getCoordinateForT = function (t, p1, p2) { 2626 | var c = 3 * p1, 2627 | b = 3 * (p2 - p1) - c, 2628 | a = 1 - c - b; 2629 | 2630 | return ((a * t + b) * t + c) * t; 2631 | }; 2632 | 2633 | CubicBezier.prototype._getCoordinateDerivateForT = function (t, p1, p2) { 2634 | var c = 3 * p1, 2635 | b = 3 * (p2 - p1) - c, 2636 | a = 1 - c - b; 2637 | 2638 | return (3 * a * t + 2 * b) * t + c; 2639 | }; 2640 | 2641 | CubicBezier.prototype._getTForCoordinate = function (c, p1, p2, epsilon) { 2642 | if (!isFinite(epsilon) || epsilon <= 0) { 2643 | throw new RangeError('"epsilon" must be a number greater than 0.'); 2644 | } 2645 | var t2, i, c2, d2; 2646 | 2647 | // First try a few iterations of Newton's method -- normally very fast. 2648 | for (t2 = c, i = 0; i < 8; i = i + 1) { 2649 | c2 = this._getCoordinateForT(t2, p1, p2) - c; 2650 | if (Math.abs(c2) < epsilon) { 2651 | return t2; 2652 | } 2653 | d2 = this._getCoordinateDerivateForT(t2, p1, p2); 2654 | if (Math.abs(d2) < 1e-6) { 2655 | break; 2656 | } 2657 | t2 = t2 - c2 / d2; 2658 | } 2659 | 2660 | // Fall back to the bisection method for reliability. 2661 | t2 = c; 2662 | var t0 = 0, 2663 | t1 = 1; 2664 | 2665 | if (t2 < t0) { 2666 | return t0; 2667 | } 2668 | if (t2 > t1) { 2669 | return t1; 2670 | } 2671 | 2672 | while (t0 < t1) { 2673 | c2 = this._getCoordinateForT(t2, p1, p2); 2674 | if (Math.abs(c2 - c) < epsilon) { 2675 | return t2; 2676 | } 2677 | if (c > c2) { 2678 | t0 = t2; 2679 | } else { 2680 | t1 = t2; 2681 | } 2682 | t2 = (t1 - t0) * 0.5 + t0; 2683 | } 2684 | 2685 | // Failure. 2686 | return t2; 2687 | }; 2688 | 2689 | /** 2690 | * Computes the point for a given t value. 2691 | * 2692 | * @param {number} t 2693 | * @returns {Object} Returns an object with x and y properties 2694 | */ 2695 | CubicBezier.prototype.getPointForT = function (t) { 2696 | 2697 | // Special cases: starting and ending points 2698 | if (t === 0 || t === 1) { 2699 | return { x: t, y: t }; 2700 | } 2701 | 2702 | // Check for correct t value (must be between 0 and 1) 2703 | if (t < 0 || t > 1) { 2704 | _throwRangeError('t', t); 2705 | } 2706 | 2707 | return { 2708 | x: this._getCoordinateForT(t, this._p1.x, this._p2.x), 2709 | y: this._getCoordinateForT(t, this._p1.y, this._p2.y) 2710 | }; 2711 | }; 2712 | 2713 | CubicBezier.prototype.getTForX = function (x, epsilon) { 2714 | return this._getTForCoordinate(x, this._p1.x, this._p2.x, epsilon); 2715 | }; 2716 | 2717 | CubicBezier.prototype.getTForY = function (y, epsilon) { 2718 | return this._getTForCoordinate(y, this._p1.y, this._p2.y, epsilon); 2719 | }; 2720 | 2721 | /** 2722 | * Computes auxiliary points using De Casteljau's algorithm. 2723 | * 2724 | * @param {number} t must be greater than 0 and lower than 1. 2725 | * @returns {Object} with members i0, i1, i2 (first iteration), 2726 | * j1, j2 (second iteration) and k (the exact point for t) 2727 | */ 2728 | CubicBezier.prototype._getAuxPoints = function (t) { 2729 | if (t <= 0 || t >= 1) { 2730 | _throwRangeError('t', t); 2731 | } 2732 | 2733 | 2734 | /* First series of auxiliary points */ 2735 | 2736 | // First control point of the left curve 2737 | var i0 = { 2738 | x: t * this._p1.x, 2739 | y: t * this._p1.y 2740 | }, 2741 | i1 = { 2742 | x: this._p1.x + t * (this._p2.x - this._p1.x), 2743 | y: this._p1.y + t * (this._p2.y - this._p1.y) 2744 | }, 2745 | 2746 | // Second control point of the right curve 2747 | i2 = { 2748 | x: this._p2.x + t * (1 - this._p2.x), 2749 | y: this._p2.y + t * (1 - this._p2.y) 2750 | }; 2751 | 2752 | 2753 | /* Second series of auxiliary points */ 2754 | 2755 | // Second control point of the left curve 2756 | var j0 = { 2757 | x: i0.x + t * (i1.x - i0.x), 2758 | y: i0.y + t * (i1.y - i0.y) 2759 | }, 2760 | 2761 | // First control point of the right curve 2762 | j1 = { 2763 | x: i1.x + t * (i2.x - i1.x), 2764 | y: i1.y + t * (i2.y - i1.y) 2765 | }; 2766 | 2767 | // The division point (ending point of left curve, starting point of right curve) 2768 | var k = { 2769 | x: j0.x + t * (j1.x - j0.x), 2770 | y: j0.y + t * (j1.y - j0.y) 2771 | }; 2772 | 2773 | return { 2774 | i0: i0, 2775 | i1: i1, 2776 | i2: i2, 2777 | j0: j0, 2778 | j1: j1, 2779 | k: k 2780 | }; 2781 | }; 2782 | 2783 | /** 2784 | * Divides the bezier curve into two bezier functions. 2785 | * 2786 | * De Casteljau's algorithm is used to compute the new starting, ending, and 2787 | * control points. 2788 | * 2789 | * @param {number} t must be greater than 0 and lower than 1. 2790 | * t === 1 or t === 0 are the starting/ending points of the curve, so no 2791 | * division is needed. 2792 | * 2793 | * @returns {CubicBezier[]} Returns an array containing two bezier curves 2794 | * to the left and the right of t. 2795 | */ 2796 | CubicBezier.prototype.divideAtT = function (t) { 2797 | if (t < 0 || t > 1) { 2798 | _throwRangeError('t', t); 2799 | } 2800 | 2801 | // Special cases t = 0, t = 1: Curve can be cloned for one side, the other 2802 | // side is a linear curve (with duration 0) 2803 | if (t === 0 || t === 1) { 2804 | var curves = []; 2805 | curves[t] = CubicBezier.linear(); 2806 | curves[1 - t] = this.clone(); 2807 | return curves; 2808 | } 2809 | 2810 | var left = {}, 2811 | right = {}, 2812 | points = this._getAuxPoints(t); 2813 | 2814 | var i0 = points.i0, 2815 | i2 = points.i2, 2816 | j0 = points.j0, 2817 | j1 = points.j1, 2818 | k = points.k; 2819 | 2820 | // Normalize derived points, so that the new curves starting/ending point 2821 | // coordinates are (0, 0) respectively (1, 1) 2822 | var factorX = k.x, 2823 | factorY = k.y; 2824 | 2825 | left.p1 = { 2826 | x: i0.x / factorX, 2827 | y: i0.y / factorY 2828 | }; 2829 | left.p2 = { 2830 | x: j0.x / factorX, 2831 | y: j0.y / factorY 2832 | }; 2833 | 2834 | right.p1 = { 2835 | x: (j1.x - factorX) / (1 - factorX), 2836 | y: (j1.y - factorY) / (1 - factorY) 2837 | }; 2838 | 2839 | right.p2 = { 2840 | x: (i2.x - factorX) / (1 - factorX), 2841 | y: (i2.y - factorY) / (1 - factorY) 2842 | }; 2843 | 2844 | return [ 2845 | new CubicBezier(left.p1.x, left.p1.y, left.p2.x, left.p2.y), 2846 | new CubicBezier(right.p1.x, right.p1.y, right.p2.x, right.p2.y) 2847 | ]; 2848 | }; 2849 | 2850 | CubicBezier.prototype.divideAtX = function (x, epsilon) { 2851 | if (x < 0 || x > 1) { 2852 | _throwRangeError('x', x); 2853 | } 2854 | 2855 | var t = this.getTForX(x, epsilon); 2856 | return this.divideAtT(t); 2857 | }; 2858 | 2859 | CubicBezier.prototype.divideAtY = function (y, epsilon) { 2860 | if (y < 0 || y > 1) { 2861 | _throwRangeError('y', y); 2862 | } 2863 | 2864 | var t = this.getTForY(y, epsilon); 2865 | return this.divideAtT(t); 2866 | }; 2867 | 2868 | CubicBezier.prototype.clone = function () { 2869 | return new CubicBezier(this._p1.x, this._p1.y, this._p2.x, this._p2.y); 2870 | }; 2871 | 2872 | CubicBezier.prototype.toString = function () { 2873 | return "cubic-bezier(" + [ 2874 | this._p1.x, 2875 | this._p1.y, 2876 | this._p2.x, 2877 | this._p2.y 2878 | ].join(", ") + ")"; 2879 | }; 2880 | 2881 | CubicBezier.linear = function () { 2882 | return new CubicBezier(); 2883 | }; 2884 | 2885 | CubicBezier.ease = function () { 2886 | return new CubicBezier(0.25, 0.1, 0.25, 1.0); 2887 | }; 2888 | CubicBezier.linear = function () { 2889 | return new CubicBezier(0.0, 0.0, 1.0, 1.0); 2890 | }; 2891 | CubicBezier.easeIn = function () { 2892 | return new CubicBezier(0.42, 0, 1.0, 1.0); 2893 | }; 2894 | CubicBezier.easeOut = function () { 2895 | return new CubicBezier(0, 0, 0.58, 1.0); 2896 | }; 2897 | CubicBezier.easeInOut = function () { 2898 | return new CubicBezier(0.42, 0, 0.58, 1.0); 2899 | }; 2900 | }()); 2901 | 2902 | if (typeof define !== 'undefined' && define.amd) { 2903 | // AMD. Register as an anonymous module. 2904 | define(function() { 2905 | 'use strict'; 2906 | return { 2907 | FTScroller: FTScroller, 2908 | CubicBezier: CubicBezier 2909 | }; 2910 | }); 2911 | } else if (typeof module !== 'undefined' && module.exports) { 2912 | module.exports = function(domNode, options) { 2913 | 'use strict'; 2914 | return new FTScroller(domNode, options); 2915 | }; 2916 | 2917 | module.exports.FTScroller = FTScroller; 2918 | module.exports.CubicBezier = CubicBezier; 2919 | } 2920 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ftscroller", 3 | "version": "0.7.0", 4 | "author": "FT Labs (http://labs.ft.com/)", 5 | "description": "FTScroller is a cross-browser Javascript/CSS library to allow touch, mouse or scrollwheel scrolling within specified elements, with pagination, snapping and bouncing support.", 6 | "contributors": [ 7 | "Rowan Beentje" 8 | ], 9 | "main": "lib/ftscroller.js", 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/ftlabs/ftscroller.git" 13 | }, 14 | "keywords": [ 15 | "scroller", 16 | "scrolling", 17 | "touch", 18 | "mobile" 19 | ], 20 | "license": "MIT", 21 | "homepage": "https://github.com/ftlabs/ftscroller" 22 | } 23 | --------------------------------------------------------------------------------