diff --git a/app/assets/javascripts/app/controllers/ticket_zoom.js.coffee b/app/assets/javascripts/app/controllers/ticket_zoom.js.coffee index 4cc172810..a2f6415b5 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom.js.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom.js.coffee @@ -1,9 +1,11 @@ class App.TicketZoom extends App.Controller elements: - '.main': 'main' + '.main': 'main' + '.ticketZoom': 'ticketZoom' + '.scrollPageHeader': 'scrollPageHeader' events: - 'click .js-submit': 'submit' + 'click .js-submit': 'submit' 'click .js-bookmark': 'bookmark' constructor: (params) -> @@ -59,24 +61,21 @@ class App.TicketZoom extends App.Controller show: => App.OnlineNotification.seen( 'Ticket', @ticket_id ) @navupdate '#' - if @scrollHeader - @scrollHeader.continue() + @positionPageHeaderStart() hide: => - if @scrollHeader - @scrollHeader.pause() + @positionPageHeaderStop() changed: => formCurrent = @formParam( @el.find('.edit') ) - ticket = App.Ticket.find(@ticket_id).attributes() - modelDiff = @getDiff( ticket, formCurrent ) + ticket = App.Ticket.find(@ticket_id).attributes() + modelDiff = @getDiff( ticket, formCurrent ) return false if !modelDiff || _.isEmpty( modelDiff ) return true release: => - # nothing @autosaveStop() - @scrollHeader.destroy() if @scrollHeader + @positionPageHeaderStop() fetch: (ticket_id, force) -> @@ -149,6 +148,36 @@ class App.TicketZoom extends App.Controller # render page @render(force) + positionPageHeaderStart: => + + # scroll is also fired on window resize, if element scroll is changed + @main.bind( + 'scroll' + @positionPageHeaderUpdate + ) + + positionPageHeaderStop: => + @main.unbind('scroll', @positionPageHeaderUpdate) + + positionPageHeaderUpdate: => + pageHeader = @scrollPageHeader.height() + mainScrollHeigth = @main.prop('scrollHeight') + mainHeigth = @main.height() + + # if page header is possible to use, show page header + top = 0 + if mainScrollHeigth > mainHeigth + pageHeader + offset = @ticketZoom.offset() + if offset.top >= 0 + top = offset.top + + # if page header is not possible to use - mainScrollHeigth to low - hide page header + else + top = pageHeader + + #console.log('TOP', top, @ticket.id, new Date) + @scrollPageHeader.css('transform', "translateY(-#{top}px)") + render: (force) => # update taskbar with new meta data @@ -404,21 +433,11 @@ class App.TicketZoom extends App.Controller @scrollToBottom() - @bindScrollPageHeader() + @positionPageHeaderStart() scrollToBottom: => @main.scrollTop( @main.prop('scrollHeight') ) - bindScrollPageHeader: -> - pageHeader = @$('.page-header') - scrollBody = @main.prop('scrollHeight') - @main.height() - - if scrollBody > pageHeader.height() - # TODO: recalculate the distance when adding a comment - @scrollHeader = skrollr.init - forceHeight: false - holder: @main.get(0) - autosaveStop: => @autosaveLast = {} @clearInterval( 'autosave' ) diff --git a/app/assets/javascripts/app/lib/animations/skrollr.js b/app/assets/javascripts/app/lib/animations/skrollr.js deleted file mode 100644 index 47d5cbc2a..000000000 --- a/app/assets/javascripts/app/lib/animations/skrollr.js +++ /dev/null @@ -1,1789 +0,0 @@ -/*! - * skrollr core - * - * Alexander Prinzhorn - https://github.com/Prinzhorn/skrollr - * - * Modified: Felix Niklas – add 'holder' option for scrolling inside a div - * - * Free to use under terms of MIT license - */ -window.id = 0; -(function(window, document, undefined) { - 'use strict'; - - /* - * Global api. - */ - var skrollr = { - get: function() { - return _instance; - }, - //Main entry point. - init: function(options) { - new Skrollr(options); - }, - VERSION: '0.6.26' - }; - - //Minify optimization. - var hasProp = Object.prototype.hasOwnProperty; - var Math = window.Math; - var getStyle = window.getComputedStyle; - - //They will be filled when skrollr gets initialized. - var documentElement; - var body; - - var EVENT_TOUCHSTART = 'touchstart'; - var EVENT_TOUCHMOVE = 'touchmove'; - var EVENT_TOUCHCANCEL = 'touchcancel'; - var EVENT_TOUCHEND = 'touchend'; - - var SKROLLABLE_CLASS = 'skrollable'; - var SKROLLABLE_BEFORE_CLASS = SKROLLABLE_CLASS + '-before'; - var SKROLLABLE_BETWEEN_CLASS = SKROLLABLE_CLASS + '-between'; - var SKROLLABLE_AFTER_CLASS = SKROLLABLE_CLASS + '-after'; - - var SKROLLR_CLASS = 'skrollr'; - var NO_SKROLLR_CLASS = 'no-' + SKROLLR_CLASS; - var SKROLLR_DESKTOP_CLASS = SKROLLR_CLASS + '-desktop'; - var SKROLLR_MOBILE_CLASS = SKROLLR_CLASS + '-mobile'; - - var DEFAULT_EASING = 'linear'; - var DEFAULT_DURATION = 1000;//ms - var DEFAULT_MOBILE_DECELERATION = 0.004;//pixel/ms² - - var DEFAULT_SMOOTH_SCROLLING_DURATION = 200;//ms - - var ANCHOR_START = 'start'; - var ANCHOR_END = 'end'; - var ANCHOR_CENTER = 'center'; - var ANCHOR_BOTTOM = 'bottom'; - - //The property which will be added to the DOM element to hold the ID of the skrollable. - var SKROLLABLE_ID_DOM_PROPERTY = '___skrollable_id'; - - var rxTouchIgnoreTags = /^(?:input|textarea|button|select)$/i; - - var rxTrim = /^\s+|\s+$/g; - - //Find all data-attributes. data-[_constant]-[offset]-[anchor]-[anchor]. - var rxKeyframeAttribute = /^data(?:-(_\w+))?(?:-?(-?\d*\.?\d+p?))?(?:-?(start|end|top|center|bottom))?(?:-?(top|center|bottom))?$/; - - var rxPropValue = /\s*(@?[\w\-\[\]]+)\s*:\s*(.+?)\s*(?:;|$)/gi; - - //Easing function names follow the property in square brackets. - var rxPropEasing = /^(@?[a-z\-]+)\[(\w+)\]$/; - - var rxCamelCase = /-([a-z0-9_])/g; - var rxCamelCaseFn = function(str, letter) { - return letter.toUpperCase(); - }; - - //Numeric values with optional sign. - var rxNumericValue = /[\-+]?[\d]*\.?[\d]+/g; - - //Used to replace occurences of {?} with a number. - var rxInterpolateString = /\{\?\}/g; - - //Finds rgb(a) colors, which don't use the percentage notation. - var rxRGBAIntegerColor = /rgba?\(\s*-?\d+\s*,\s*-?\d+\s*,\s*-?\d+/g; - - //Finds all gradients. - var rxGradient = /[a-z\-]+-gradient/g; - - //Vendor prefix. Will be set once skrollr gets initialized. - var theCSSPrefix = ''; - var theDashedCSSPrefix = ''; - - //Will be called once (when skrollr gets initialized). - var detectCSSPrefix = function() { - //Only relevant prefixes. May be extended. - //Could be dangerous if there will ever be a CSS property which actually starts with "ms". Don't hope so. - var rxPrefixes = /^(?:O|Moz|webkit|ms)|(?:-(?:o|moz|webkit|ms)-)/; - - //Detect prefix for current browser by finding the first property using a prefix. - if(!getStyle) { - return; - } - - var style = getStyle(body, null); - - for(var k in style) { - //We check the key and if the key is a number, we check the value as well, because safari's getComputedStyle returns some weird array-like thingy. - theCSSPrefix = (k.match(rxPrefixes) || (+k == k && style[k].match(rxPrefixes))); - - if(theCSSPrefix) { - break; - } - } - - //Did we even detect a prefix? - if(!theCSSPrefix) { - theCSSPrefix = theDashedCSSPrefix = ''; - - return; - } - - theCSSPrefix = theCSSPrefix[0]; - - //We could have detected either a dashed prefix or this camelCaseish-inconsistent stuff. - if(theCSSPrefix.slice(0,1) === '-') { - theDashedCSSPrefix = theCSSPrefix; - - //There's no logic behind these. Need a look up. - theCSSPrefix = ({ - '-webkit-': 'webkit', - '-moz-': 'Moz', - '-ms-': 'ms', - '-o-': 'O' - })[theCSSPrefix]; - } else { - theDashedCSSPrefix = '-' + theCSSPrefix.toLowerCase() + '-'; - } - }; - - var polyfillRAF = function() { - var requestAnimFrame = window.requestAnimationFrame || window[theCSSPrefix.toLowerCase() + 'RequestAnimationFrame']; - - var lastTime = _now(); - - if(_isMobile || !requestAnimFrame) { - requestAnimFrame = function(callback) { - //How long did it take to render? - var deltaTime = _now() - lastTime; - var delay = Math.max(0, 1000 / 60 - deltaTime); - - return window.setTimeout(function() { - lastTime = _now(); - callback(); - }, delay); - }; - } - - return requestAnimFrame; - }; - - var polyfillCAF = function() { - var cancelAnimFrame = window.cancelAnimationFrame || window[theCSSPrefix.toLowerCase() + 'CancelAnimationFrame']; - - if(_isMobile || !cancelAnimFrame) { - cancelAnimFrame = function(timeout) { - return window.clearTimeout(timeout); - }; - } - - return cancelAnimFrame; - }; - - //Built-in easing functions. - var easings = { - begin: function() { - return 0; - }, - end: function() { - return 1; - }, - linear: function(p) { - return p; - }, - quadratic: function(p) { - return p * p; - }, - cubic: function(p) { - return p * p * p; - }, - swing: function(p) { - return (-Math.cos(p * Math.PI) / 2) + 0.5; - }, - sqrt: function(p) { - return Math.sqrt(p); - }, - outCubic: function(p) { - return (Math.pow((p - 1), 3) + 1); - }, - //see https://www.desmos.com/calculator/tbr20s8vd2 for how I did this - bounce: function(p) { - var a; - - if(p <= 0.5083) { - a = 3; - } else if(p <= 0.8489) { - a = 9; - } else if(p <= 0.96208) { - a = 27; - } else if(p <= 0.99981) { - a = 91; - } else { - return 1; - } - - return 1 - Math.abs(3 * Math.cos(p * a * 1.028) / a); - } - }; - - /** - * Constructor. - */ - function Skrollr(options) { - documentElement = options.holder || document.documentElement; - body = document.body; - - detectCSSPrefix(); - - _instance = this; - - this.id = window.id++; - - options = options || {}; - - _constants = options.constants || {}; - - //We allow defining custom easings or overwrite existing. - if(options.easing) { - for(var e in options.easing) { - easings[e] = options.easing[e]; - } - } - - _edgeStrategy = options.edgeStrategy || 'set'; - - _listeners = { - //Function to be called right before rendering. - beforerender: options.beforerender, - - //Function to be called right after finishing rendering. - render: options.render, - - //Function to be called whenever an element with the `data-emit-events` attribute passes a keyframe. - keyframe: options.keyframe - }; - - //forceHeight is true by default - _forceHeight = options.forceHeight !== false; - - if(_forceHeight) { - _scale = options.scale || 1; - } - - _mobileDeceleration = options.mobileDeceleration || DEFAULT_MOBILE_DECELERATION; - - _smoothScrollingEnabled = options.smoothScrolling !== false; - _smoothScrollingDuration = options.smoothScrollingDuration || DEFAULT_SMOOTH_SCROLLING_DURATION; - - //Dummy object. Will be overwritten in the _render method when smooth scrolling is calculated. - _smoothScrolling = { - targetTop: _instance.getScrollTop() - }; - - //A custom check function may be passed. - _isMobile = ((options.mobileCheck || function() { - return (/Android|iPhone|iPad|iPod|BlackBerry/i).test(navigator.userAgent || navigator.vendor || window.opera); - })()); - - if(_isMobile) { - _skrollrBody = document.getElementById('skrollr-body'); - - //Detect 3d transform if there's a skrollr-body (only needed for #skrollr-body). - if(_skrollrBody) { - _detect3DTransforms(); - } - - _initMobile(); - _updateClass(documentElement, [SKROLLR_CLASS, SKROLLR_MOBILE_CLASS], [NO_SKROLLR_CLASS]); - } else { - _updateClass(documentElement, [SKROLLR_CLASS, SKROLLR_DESKTOP_CLASS], [NO_SKROLLR_CLASS]); - } - - //Triggers parsing of elements and a first reflow. - _instance.refresh(); - - _addEvent(window, 'resize orientationchange', function() { - var width = documentElement.clientWidth; - var height = documentElement.clientHeight; - - //Only reflow if the size actually changed (#271). - if(height !== _lastViewportHeight || width !== _lastViewportWidth) { - _lastViewportHeight = height; - _lastViewportWidth = width; - - _requestReflow = true; - } - }); - - //Let's go. - this.animloop(); - - return _instance; - } - - Skrollr.prototype.animloop = function() { - var requestAnimFrame = polyfillRAF(); - _render(); - // console.log("rendering", this.id); - if(!this.paused) - _animFrame = requestAnimFrame(_instance.animloop.bind(this)); - } - - Skrollr.prototype.pause = function() { - cancelAnimFrame(_animFrame); - this.paused = true; - } - - Skrollr.prototype.continue = function() { - this.paused = false; - this.animloop(); - } - - /** - * (Re)parses some or all elements. - */ - Skrollr.prototype.refresh = function(elements) { - var elementIndex; - var elementsLength; - var ignoreID = false; - - //Completely reparse anything without argument. - if(elements === undefined) { - //Ignore that some elements may already have a skrollable ID. - ignoreID = true; - - _skrollables = []; - _skrollableIdCounter = 0; - - elements = document.getElementsByTagName('*'); - } else if(elements.length === undefined) { - //We also accept a single element as parameter. - elements = [elements]; - } - - elementIndex = 0; - elementsLength = elements.length; - - for(; elementIndex < elementsLength; elementIndex++) { - var el = elements[elementIndex]; - var anchorTarget = el; - var keyFrames = []; - - //If this particular element should be smooth scrolled. - var smoothScrollThis = _smoothScrollingEnabled; - - //The edge strategy for this particular element. - var edgeStrategy = _edgeStrategy; - - //If this particular element should emit keyframe events. - var emitEvents = false; - - //If we're reseting the counter, remove any old element ids that may be hanging around. - if(ignoreID && SKROLLABLE_ID_DOM_PROPERTY in el) { - delete el[SKROLLABLE_ID_DOM_PROPERTY]; - } - - if(!el.attributes) { - continue; - } - - //Iterate over all attributes and search for key frame attributes. - var attributeIndex = 0; - var attributesLength = el.attributes.length; - - for (; attributeIndex < attributesLength; attributeIndex++) { - var attr = el.attributes[attributeIndex]; - - if(attr.name === 'data-anchor-target') { - anchorTarget = document.querySelector(attr.value); - - if(anchorTarget === null) { - throw 'Unable to find anchor target "' + attr.value + '"'; - } - - continue; - } - - //Global smooth scrolling can be overridden by the element attribute. - if(attr.name === 'data-smooth-scrolling') { - smoothScrollThis = attr.value !== 'off'; - - continue; - } - - //Global edge strategy can be overridden by the element attribute. - if(attr.name === 'data-edge-strategy') { - edgeStrategy = attr.value; - - continue; - } - - //Is this element tagged with the `data-emit-events` attribute? - if(attr.name === 'data-emit-events') { - emitEvents = true; - - continue; - } - - var match = attr.name.match(rxKeyframeAttribute); - - if(match === null) { - continue; - } - - var kf = { - props: attr.value, - //Point back to the element as well. - element: el, - //The name of the event which this keyframe will fire, if emitEvents is - eventType: attr.name.replace(rxCamelCase, rxCamelCaseFn) - }; - - keyFrames.push(kf); - - var constant = match[1]; - - if(constant) { - //Strip the underscore prefix. - kf.constant = constant.substr(1); - } - - //Get the key frame offset. - var offset = match[2]; - - //Is it a percentage offset? - if(/p$/.test(offset)) { - kf.isPercentage = true; - kf.offset = (offset.slice(0, -1) | 0) / 100; - } else { - kf.offset = (offset | 0); - } - - var anchor1 = match[3]; - - //If second anchor is not set, the first will be taken for both. - var anchor2 = match[4] || anchor1; - - //"absolute" (or "classic") mode, where numbers mean absolute scroll offset. - if(!anchor1 || anchor1 === ANCHOR_START || anchor1 === ANCHOR_END) { - kf.mode = 'absolute'; - - //data-end needs to be calculated after all key frames are known. - if(anchor1 === ANCHOR_END) { - kf.isEnd = true; - } else if(!kf.isPercentage) { - //For data-start we can already set the key frame w/o calculations. - //#59: "scale" options should only affect absolute mode. - kf.offset = kf.offset * _scale; - } - } - //"relative" mode, where numbers are relative to anchors. - else { - kf.mode = 'relative'; - kf.anchors = [anchor1, anchor2]; - } - } - - //Does this element have key frames? - if(!keyFrames.length) { - continue; - } - - //Will hold the original style and class attributes before we controlled the element (see #80). - var styleAttr, classAttr; - - var id; - - if(!ignoreID && SKROLLABLE_ID_DOM_PROPERTY in el) { - //We already have this element under control. Grab the corresponding skrollable id. - id = el[SKROLLABLE_ID_DOM_PROPERTY]; - styleAttr = _skrollables[id].styleAttr; - classAttr = _skrollables[id].classAttr; - } else { - //It's an unknown element. Asign it a new skrollable id. - id = (el[SKROLLABLE_ID_DOM_PROPERTY] = _skrollableIdCounter++); - styleAttr = el.style.cssText; - classAttr = _getClass(el); - } - - _skrollables[id] = { - element: el, - styleAttr: styleAttr, - classAttr: classAttr, - anchorTarget: anchorTarget, - keyFrames: keyFrames, - smoothScrolling: smoothScrollThis, - edgeStrategy: edgeStrategy, - emitEvents: emitEvents, - lastFrameIndex: -1 - }; - - _updateClass(el, [SKROLLABLE_CLASS], []); - } - - //Reflow for the first time. - _reflow(); - - //Now that we got all key frame numbers right, actually parse the properties. - elementIndex = 0; - elementsLength = elements.length; - - for(; elementIndex < elementsLength; elementIndex++) { - var sk = _skrollables[elements[elementIndex][SKROLLABLE_ID_DOM_PROPERTY]]; - - if(sk === undefined) { - continue; - } - - //Parse the property string to objects - _parseProps(sk); - - //Fill key frames with missing properties from left and right - _fillProps(sk); - } - - return _instance; - }; - - /** - * Transform "relative" mode to "absolute" mode. - * That is, calculate anchor position and offset of element. - */ - Skrollr.prototype.relativeToAbsolute = function(element, viewportAnchor, elementAnchor) { - var viewportHeight = documentElement.clientHeight; - var box = element.getBoundingClientRect(); - var absolute = box.top; - - //#100: IE doesn't supply "height" with getBoundingClientRect. - var boxHeight = box.bottom - box.top; - - if(viewportAnchor === ANCHOR_BOTTOM) { - absolute -= viewportHeight; - } else if(viewportAnchor === ANCHOR_CENTER) { - absolute -= viewportHeight / 2; - } - - if(elementAnchor === ANCHOR_BOTTOM) { - absolute += boxHeight; - } else if(elementAnchor === ANCHOR_CENTER) { - absolute += boxHeight / 2; - } - - //Compensate scrolling since getBoundingClientRect is relative to viewport. - absolute += _instance.getScrollTop(); - - return (absolute + 0.5) | 0; - }; - - /** - * Animates scroll top to new position. - */ - Skrollr.prototype.animateTo = function(top, options) { - options = options || {}; - - var now = _now(); - var scrollTop = _instance.getScrollTop(); - - //Setting this to a new value will automatically cause the current animation to stop, if any. - _scrollAnimation = { - startTop: scrollTop, - topDiff: top - scrollTop, - targetTop: top, - duration: options.duration || DEFAULT_DURATION, - startTime: now, - endTime: now + (options.duration || DEFAULT_DURATION), - easing: easings[options.easing || DEFAULT_EASING], - done: options.done - }; - - //Don't queue the animation if there's nothing to animate. - if(!_scrollAnimation.topDiff) { - if(_scrollAnimation.done) { - _scrollAnimation.done.call(_instance, false); - } - - _scrollAnimation = undefined; - } - - return _instance; - }; - - /** - * Stops animateTo animation. - */ - Skrollr.prototype.stopAnimateTo = function() { - if(_scrollAnimation && _scrollAnimation.done) { - _scrollAnimation.done.call(_instance, true); - } - - _scrollAnimation = undefined; - }; - - /** - * Returns if an animation caused by animateTo is currently running. - */ - Skrollr.prototype.isAnimatingTo = function() { - return !!_scrollAnimation; - }; - - Skrollr.prototype.isMobile = function() { - return _isMobile; - }; - - Skrollr.prototype.setScrollTop = function(top, force) { - _forceRender = (force === true); - - if(_isMobile) { - _mobileOffset = Math.min(Math.max(top, 0), _maxKeyFrame); - } else { - documentElement.scrollTop = top; - } - - return _instance; - }; - - Skrollr.prototype.getScrollTop = function() { - if(_isMobile) { - return _mobileOffset; - } else { - return documentElement.scrollTop || 0; - } - }; - - Skrollr.prototype.getMaxScrollTop = function() { - return _maxKeyFrame; - }; - - Skrollr.prototype.on = function(name, fn) { - _listeners[name] = fn; - - return _instance; - }; - - Skrollr.prototype.off = function(name) { - delete _listeners[name]; - - return _instance; - }; - - Skrollr.prototype.destroy = function() { - var cancelAnimFrame = polyfillCAF(); - cancelAnimFrame(_animFrame); - _removeAllEvents(); - - _updateClass(documentElement, [NO_SKROLLR_CLASS], [SKROLLR_CLASS, SKROLLR_DESKTOP_CLASS, SKROLLR_MOBILE_CLASS]); - - var skrollableIndex = 0; - var skrollablesLength = _skrollables.length; - - for(; skrollableIndex < skrollablesLength; skrollableIndex++) { - _reset(_skrollables[skrollableIndex].element); - } - - documentElement.style.overflow = body.style.overflow = ''; - documentElement.style.height = body.style.height = ''; - - if(_skrollrBody) { - skrollr.setStyle(_skrollrBody, 'transform', 'none'); - } - - _instance = undefined; - _skrollrBody = undefined; - _listeners = undefined; - _forceHeight = undefined; - _maxKeyFrame = 0; - _scale = 1; - _constants = undefined; - _mobileDeceleration = undefined; - _direction = 'down'; - _lastTop = -1; - _lastViewportWidth = 0; - _lastViewportHeight = 0; - _requestReflow = false; - _scrollAnimation = undefined; - _smoothScrollingEnabled = undefined; - _smoothScrollingDuration = undefined; - _smoothScrolling = undefined; - _forceRender = undefined; - _skrollableIdCounter = 0; - _edgeStrategy = undefined; - _isMobile = false; - _mobileOffset = 0; - _translateZ = undefined; - }; - - /* - Private methods. - */ - - var _initMobile = function() { - var initialElement; - var initialTouchY; - var initialTouchX; - var currentElement; - var currentTouchY; - var currentTouchX; - var lastTouchY; - var deltaY; - - var initialTouchTime; - var currentTouchTime; - var lastTouchTime; - var deltaTime; - - _addEvent(documentElement, [EVENT_TOUCHSTART, EVENT_TOUCHMOVE, EVENT_TOUCHCANCEL, EVENT_TOUCHEND].join(' '), function(e) { - var touch = e.changedTouches[0]; - - currentElement = e.target; - - //We don't want text nodes. - while(currentElement.nodeType === 3) { - currentElement = currentElement.parentNode; - } - - currentTouchY = touch.clientY; - currentTouchX = touch.clientX; - currentTouchTime = e.timeStamp; - - if(!rxTouchIgnoreTags.test(currentElement.tagName)) { - e.preventDefault(); - } - - switch(e.type) { - case EVENT_TOUCHSTART: - //The last element we tapped on. - if(initialElement) { - initialElement.blur(); - } - - _instance.stopAnimateTo(); - - initialElement = currentElement; - - initialTouchY = lastTouchY = currentTouchY; - initialTouchX = currentTouchX; - initialTouchTime = currentTouchTime; - - break; - case EVENT_TOUCHMOVE: - //Prevent default event on touchIgnore elements in case they don't have focus yet. - if(rxTouchIgnoreTags.test(currentElement.tagName) && document.activeElement !== currentElement) { - e.preventDefault(); - } - - deltaY = currentTouchY - lastTouchY; - deltaTime = currentTouchTime - lastTouchTime; - - _instance.setScrollTop(_mobileOffset - deltaY, true); - - lastTouchY = currentTouchY; - lastTouchTime = currentTouchTime; - break; - default: - case EVENT_TOUCHCANCEL: - case EVENT_TOUCHEND: - var distanceY = initialTouchY - currentTouchY; - var distanceX = initialTouchX - currentTouchX; - var distance2 = distanceX * distanceX + distanceY * distanceY; - - //Check if it was more like a tap (moved less than 7px). - if(distance2 < 49) { - if(!rxTouchIgnoreTags.test(initialElement.tagName)) { - initialElement.focus(); - - //It was a tap, click the element. - var clickEvent = document.createEvent('MouseEvents'); - clickEvent.initMouseEvent('click', true, true, e.view, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, 0, null); - initialElement.dispatchEvent(clickEvent); - } - - return; - } - - initialElement = undefined; - - var speed = deltaY / deltaTime; - - //Cap speed at 3 pixel/ms. - speed = Math.max(Math.min(speed, 3), -3); - - var duration = Math.abs(speed / _mobileDeceleration); - var targetOffset = speed * duration + 0.5 * _mobileDeceleration * duration * duration; - var targetTop = _instance.getScrollTop() - targetOffset; - - //Relative duration change for when scrolling above bounds. - var targetRatio = 0; - - //Change duration proportionally when scrolling would leave bounds. - if(targetTop > _maxKeyFrame) { - targetRatio = (_maxKeyFrame - targetTop) / targetOffset; - - targetTop = _maxKeyFrame; - } else if(targetTop < 0) { - targetRatio = -targetTop / targetOffset; - - targetTop = 0; - } - - duration = duration * (1 - targetRatio); - - _instance.animateTo((targetTop + 0.5) | 0, {easing: 'outCubic', duration: duration}); - break; - } - }); - - //Just in case there has already been some native scrolling, reset it. - window.scrollTo(0, 0); - documentElement.style.overflow = body.style.overflow = 'hidden'; - }; - - /** - * Updates key frames which depend on others / need to be updated on resize. - * That is "end" in "absolute" mode and all key frames in "relative" mode. - * Also handles constants, because they may change on resize. - */ - var _updateDependentKeyFrames = function() { - var viewportHeight = documentElement.clientHeight; - var processedConstants = _processConstants(); - var skrollable; - var element; - var anchorTarget; - var keyFrames; - var keyFrameIndex; - var keyFramesLength; - var kf; - var skrollableIndex; - var skrollablesLength; - var offset; - var constantValue; - - //First process all relative-mode elements and find the max key frame. - skrollableIndex = 0; - skrollablesLength = _skrollables.length; - - for(; skrollableIndex < skrollablesLength; skrollableIndex++) { - skrollable = _skrollables[skrollableIndex]; - element = skrollable.element; - anchorTarget = skrollable.anchorTarget; - keyFrames = skrollable.keyFrames; - - keyFrameIndex = 0; - keyFramesLength = keyFrames.length; - - for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) { - kf = keyFrames[keyFrameIndex]; - - offset = kf.offset; - constantValue = processedConstants[kf.constant] || 0; - - kf.frame = offset; - - if(kf.isPercentage) { - //Convert the offset to percentage of the viewport height. - offset = offset * viewportHeight; - - //Absolute + percentage mode. - kf.frame = offset; - } - - if(kf.mode === 'relative') { - _reset(element); - - kf.frame = _instance.relativeToAbsolute(anchorTarget, kf.anchors[0], kf.anchors[1]) - offset; - - _reset(element, true); - } - - kf.frame += constantValue; - - //Only search for max key frame when forceHeight is enabled. - if(_forceHeight) { - //Find the max key frame, but don't use one of the data-end ones for comparison. - if(!kf.isEnd && kf.frame > _maxKeyFrame) { - _maxKeyFrame = kf.frame; - } - } - } - } - - //#133: The document can be larger than the maxKeyFrame we found. - _maxKeyFrame = Math.max(_maxKeyFrame, _getDocumentHeight()); - - //Now process all data-end keyframes. - skrollableIndex = 0; - skrollablesLength = _skrollables.length; - - for(; skrollableIndex < skrollablesLength; skrollableIndex++) { - skrollable = _skrollables[skrollableIndex]; - keyFrames = skrollable.keyFrames; - - keyFrameIndex = 0; - keyFramesLength = keyFrames.length; - - for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) { - kf = keyFrames[keyFrameIndex]; - - constantValue = processedConstants[kf.constant] || 0; - - if(kf.isEnd) { - kf.frame = _maxKeyFrame - kf.offset + constantValue; - } - } - - skrollable.keyFrames.sort(_keyFrameComparator); - } - }; - - /** - * Calculates and sets the style properties for the element at the given frame. - * @param fakeFrame The frame to render at when smooth scrolling is enabled. - * @param actualFrame The actual frame we are at. - */ - var _calcSteps = function(fakeFrame, actualFrame) { - //Iterate over all skrollables. - var skrollableIndex = 0; - var skrollablesLength = _skrollables.length; - - for(; skrollableIndex < skrollablesLength; skrollableIndex++) { - var skrollable = _skrollables[skrollableIndex]; - var element = skrollable.element; - var frame = skrollable.smoothScrolling ? fakeFrame : actualFrame; - var frames = skrollable.keyFrames; - var framesLength = frames.length; - var firstFrame = frames[0]; - var lastFrame = frames[frames.length - 1]; - var beforeFirst = frame < firstFrame.frame; - var afterLast = frame > lastFrame.frame; - var firstOrLastFrame = beforeFirst ? firstFrame : lastFrame; - var emitEvents = skrollable.emitEvents; - var lastFrameIndex = skrollable.lastFrameIndex; - var key; - var value; - - //If we are before/after the first/last frame, set the styles according to the given edge strategy. - if(beforeFirst || afterLast) { - //Check if we already handled this edge case last time. - //Note: using setScrollTop it's possible that we jumped from one edge to the other. - if(beforeFirst && skrollable.edge === -1 || afterLast && skrollable.edge === 1) { - continue; - } - - //Add the skrollr-before or -after class. - if(beforeFirst) { - _updateClass(element, [SKROLLABLE_BEFORE_CLASS], [SKROLLABLE_AFTER_CLASS, SKROLLABLE_BETWEEN_CLASS]); - - //This handles the special case where we exit the first keyframe. - if(emitEvents && lastFrameIndex > -1) { - _emitEvent(element, firstFrame.eventType, _direction); - skrollable.lastFrameIndex = -1; - } - } else { - _updateClass(element, [SKROLLABLE_AFTER_CLASS], [SKROLLABLE_BEFORE_CLASS, SKROLLABLE_BETWEEN_CLASS]); - - //This handles the special case where we exit the last keyframe. - if(emitEvents && lastFrameIndex < framesLength) { - _emitEvent(element, lastFrame.eventType, _direction); - skrollable.lastFrameIndex = framesLength; - } - } - - //Remember that we handled the edge case (before/after the first/last keyframe). - skrollable.edge = beforeFirst ? -1 : 1; - - switch(skrollable.edgeStrategy) { - case 'reset': - _reset(element); - continue; - case 'ease': - //Handle this case like it would be exactly at first/last keyframe and just pass it on. - frame = firstOrLastFrame.frame; - break; - default: - case 'set': - var props = firstOrLastFrame.props; - - for(key in props) { - if(hasProp.call(props, key)) { - value = _interpolateString(props[key].value); - - //Set style or attribute. - if(key.indexOf('@') === 0) { - element.setAttribute(key.substr(1), value); - } else { - skrollr.setStyle(element, key, value); - } - } - } - - continue; - } - } else { - //Did we handle an edge last time? - if(skrollable.edge !== 0) { - _updateClass(element, [SKROLLABLE_CLASS, SKROLLABLE_BETWEEN_CLASS], [SKROLLABLE_BEFORE_CLASS, SKROLLABLE_AFTER_CLASS]); - skrollable.edge = 0; - } - } - - //Find out between which two key frames we are right now. - var keyFrameIndex = 0; - - for(; keyFrameIndex < framesLength - 1; keyFrameIndex++) { - if(frame >= frames[keyFrameIndex].frame && frame <= frames[keyFrameIndex + 1].frame) { - var left = frames[keyFrameIndex]; - var right = frames[keyFrameIndex + 1]; - - for(key in left.props) { - if(hasProp.call(left.props, key)) { - var progress = (frame - left.frame) / (right.frame - left.frame); - - //Transform the current progress using the given easing function. - progress = left.props[key].easing(progress); - - //Interpolate between the two values - value = _calcInterpolation(left.props[key].value, right.props[key].value, progress); - - value = _interpolateString(value); - - //Set style or attribute. - if(key.indexOf('@') === 0) { - element.setAttribute(key.substr(1), value); - } else { - skrollr.setStyle(element, key, value); - } - } - } - - //Are events enabled on this element? - //This code handles the usual cases of scrolling through different keyframes. - //The special cases of before first and after last keyframe are handled above. - if(emitEvents) { - //Did we pass a new keyframe? - if(lastFrameIndex !== keyFrameIndex) { - if(_direction === 'down') { - _emitEvent(element, left.eventType, _direction); - } else { - _emitEvent(element, right.eventType, _direction); - } - - skrollable.lastFrameIndex = keyFrameIndex; - } - } - - break; - } - } - } - }; - - /** - * Renders all elements. - */ - var _render = function() { - if(_requestReflow) { - _requestReflow = false; - _reflow(); - } - - //We may render something else than the actual scrollbar position. - var renderTop = _instance.getScrollTop(); - - //If there's an animation, which ends in current render call, call the callback after rendering. - var afterAnimationCallback; - var now = _now(); - var progress; - - //Before actually rendering handle the scroll animation, if any. - if(_scrollAnimation) { - //It's over - if(now >= _scrollAnimation.endTime) { - renderTop = _scrollAnimation.targetTop; - afterAnimationCallback = _scrollAnimation.done; - _scrollAnimation = undefined; - } else { - //Map the current progress to the new progress using given easing function. - progress = _scrollAnimation.easing((now - _scrollAnimation.startTime) / _scrollAnimation.duration); - - renderTop = (_scrollAnimation.startTop + progress * _scrollAnimation.topDiff) | 0; - } - - _instance.setScrollTop(renderTop, true); - } - //Smooth scrolling only if there's no animation running and if we're not forcing the rendering. - else if(!_forceRender) { - var smoothScrollingDiff = _smoothScrolling.targetTop - renderTop; - - //The user scrolled, start new smooth scrolling. - if(smoothScrollingDiff) { - _smoothScrolling = { - startTop: _lastTop, - topDiff: renderTop - _lastTop, - targetTop: renderTop, - startTime: _lastRenderCall, - endTime: _lastRenderCall + _smoothScrollingDuration - }; - } - - //Interpolate the internal scroll position (not the actual scrollbar). - if(now <= _smoothScrolling.endTime) { - //Map the current progress to the new progress using easing function. - progress = easings.sqrt((now - _smoothScrolling.startTime) / _smoothScrollingDuration); - - renderTop = (_smoothScrolling.startTop + progress * _smoothScrolling.topDiff) | 0; - } - } - - //That's were we actually "scroll" on mobile. - if(_isMobile && _skrollrBody) { - //Set the transform ("scroll it"). - skrollr.setStyle(_skrollrBody, 'transform', 'translate(0, ' + -(_mobileOffset) + 'px) ' + _translateZ); - } - - //Did the scroll position even change? - if(_forceRender || _lastTop !== renderTop) { - //Remember in which direction are we scrolling? - _direction = (renderTop > _lastTop) ? 'down' : (renderTop < _lastTop ? 'up' : _direction); - - _forceRender = false; - - var listenerParams = { - curTop: renderTop, - lastTop: _lastTop, - maxTop: _maxKeyFrame, - direction: _direction - }; - - //Tell the listener we are about to render. - var continueRendering = _listeners.beforerender && _listeners.beforerender.call(_instance, listenerParams); - - //The beforerender listener function is able the cancel rendering. - if(continueRendering !== false) { - //Now actually interpolate all the styles. - _calcSteps(renderTop, _instance.getScrollTop()); - - //Remember when we last rendered. - _lastTop = renderTop; - - if(_listeners.render) { - _listeners.render.call(_instance, listenerParams); - } - } - - if(afterAnimationCallback) { - afterAnimationCallback.call(_instance, false); - } - } - - _lastRenderCall = now; - }; - - /** - * Parses the properties for each key frame of the given skrollable. - */ - var _parseProps = function(skrollable) { - //Iterate over all key frames - var keyFrameIndex = 0; - var keyFramesLength = skrollable.keyFrames.length; - - for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) { - var frame = skrollable.keyFrames[keyFrameIndex]; - var easing; - var value; - var prop; - var props = {}; - - var match; - - while((match = rxPropValue.exec(frame.props)) !== null) { - prop = match[1]; - value = match[2]; - - easing = prop.match(rxPropEasing); - - //Is there an easing specified for this prop? - if(easing !== null) { - prop = easing[1]; - easing = easing[2]; - } else { - easing = DEFAULT_EASING; - } - - //Exclamation point at first position forces the value to be taken literal. - value = value.indexOf('!') ? _parseProp(value) : [value.slice(1)]; - - //Save the prop for this key frame with his value and easing function - props[prop] = { - value: value, - easing: easings[easing] - }; - } - - frame.props = props; - } - }; - - /** - * Parses a value extracting numeric values and generating a format string - * for later interpolation of the new values in old string. - * - * @param val The CSS value to be parsed. - * @return Something like ["rgba(?%,?%, ?%,?)", 100, 50, 0, .7] - * where the first element is the format string later used - * and all following elements are the numeric value. - */ - var _parseProp = function(val) { - var numbers = []; - - //One special case, where floats don't work. - //We replace all occurences of rgba colors - //which don't use percentage notation with the percentage notation. - rxRGBAIntegerColor.lastIndex = 0; - val = val.replace(rxRGBAIntegerColor, function(rgba) { - return rgba.replace(rxNumericValue, function(n) { - return n / 255 * 100 + '%'; - }); - }); - - //Handle prefixing of "gradient" values. - //For now only the prefixed value will be set. Unprefixed isn't supported anyway. - if(theDashedCSSPrefix) { - rxGradient.lastIndex = 0; - val = val.replace(rxGradient, function(s) { - return theDashedCSSPrefix + s; - }); - } - - //Now parse ANY number inside this string and create a format string. - val = val.replace(rxNumericValue, function(n) { - numbers.push(+n); - return '{?}'; - }); - - //Add the formatstring as first value. - numbers.unshift(val); - - return numbers; - }; - - /** - * Fills the key frames with missing left and right hand properties. - * If key frame 1 has property X and key frame 2 is missing X, - * but key frame 3 has X again, then we need to assign X to key frame 2 too. - * - * @param sk A skrollable. - */ - var _fillProps = function(sk) { - //Will collect the properties key frame by key frame - var propList = {}; - var keyFrameIndex; - var keyFramesLength; - - //Iterate over all key frames from left to right - keyFrameIndex = 0; - keyFramesLength = sk.keyFrames.length; - - for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) { - _fillPropForFrame(sk.keyFrames[keyFrameIndex], propList); - } - - //Now do the same from right to fill the last gaps - - propList = {}; - - //Iterate over all key frames from right to left - keyFrameIndex = sk.keyFrames.length - 1; - - for(; keyFrameIndex >= 0; keyFrameIndex--) { - _fillPropForFrame(sk.keyFrames[keyFrameIndex], propList); - } - }; - - var _fillPropForFrame = function(frame, propList) { - var key; - - //For each key frame iterate over all right hand properties and assign them, - //but only if the current key frame doesn't have the property by itself - for(key in propList) { - //The current frame misses this property, so assign it. - if(!hasProp.call(frame.props, key)) { - frame.props[key] = propList[key]; - } - } - - //Iterate over all props of the current frame and collect them - for(key in frame.props) { - propList[key] = frame.props[key]; - } - }; - - /** - * Calculates the new values for two given values array. - */ - var _calcInterpolation = function(val1, val2, progress) { - var valueIndex; - var val1Length = val1.length; - - //They both need to have the same length - if(val1Length !== val2.length) { - throw 'Can\'t interpolate between "' + val1[0] + '" and "' + val2[0] + '"'; - } - - //Add the format string as first element. - var interpolated = [val1[0]]; - - valueIndex = 1; - - for(; valueIndex < val1Length; valueIndex++) { - //That's the line where the two numbers are actually interpolated. - interpolated[valueIndex] = val1[valueIndex] + ((val2[valueIndex] - val1[valueIndex]) * progress); - } - - return interpolated; - }; - - /** - * Interpolates the numeric values into the format string. - */ - var _interpolateString = function(val) { - var valueIndex = 1; - - rxInterpolateString.lastIndex = 0; - - return val[0].replace(rxInterpolateString, function() { - return val[valueIndex++]; - }); - }; - - /** - * Resets the class and style attribute to what it was before skrollr manipulated the element. - * Also remembers the values it had before reseting, in order to undo the reset. - */ - var _reset = function(elements, undo) { - //We accept a single element or an array of elements. - elements = [].concat(elements); - - var skrollable; - var element; - var elementsIndex = 0; - var elementsLength = elements.length; - - for(; elementsIndex < elementsLength; elementsIndex++) { - element = elements[elementsIndex]; - skrollable = _skrollables[element[SKROLLABLE_ID_DOM_PROPERTY]]; - - //Couldn't find the skrollable for this DOM element. - if(!skrollable) { - continue; - } - - if(undo) { - //Reset class and style to the "dirty" (set by skrollr) values. - element.style.cssText = skrollable.dirtyStyleAttr; - _updateClass(element, skrollable.dirtyClassAttr); - } else { - //Remember the "dirty" (set by skrollr) class and style. - skrollable.dirtyStyleAttr = element.style.cssText; - skrollable.dirtyClassAttr = _getClass(element); - - //Reset class and style to what it originally was. - element.style.cssText = skrollable.styleAttr; - _updateClass(element, skrollable.classAttr); - } - } - }; - - /** - * Detects support for 3d transforms by applying it to the skrollr-body. - */ - var _detect3DTransforms = function() { - _translateZ = 'translateZ(0)'; - skrollr.setStyle(_skrollrBody, 'transform', _translateZ); - - var computedStyle = getStyle(_skrollrBody); - var computedTransform = computedStyle.getPropertyValue('transform'); - var computedTransformWithPrefix = computedStyle.getPropertyValue(theDashedCSSPrefix + 'transform'); - var has3D = (computedTransform && computedTransform !== 'none') || (computedTransformWithPrefix && computedTransformWithPrefix !== 'none'); - - if(!has3D) { - _translateZ = ''; - } - }; - - /** - * Set the CSS property on the given element. Sets prefixed properties as well. - */ - skrollr.setStyle = function(el, prop, val) { - var style = el.style; - - //Camel case. - prop = prop.replace(rxCamelCase, rxCamelCaseFn).replace('-', ''); - - //Make sure z-index gets a . - //This is the only case we need to handle. - if(prop === 'zIndex') { - if(isNaN(val)) { - //If it's not a number, don't touch it. - //It could for example be "auto" (#351). - style[prop] = val; - } else { - //Floor the number. - style[prop] = '' + (val | 0); - } - } - //#64: "float" can't be set across browsers. Needs to use "cssFloat" for all except IE. - else if(prop === 'float') { - style.styleFloat = style.cssFloat = val; - } - else { - //Need try-catch for old IE. - try { - //Set prefixed property if there's a prefix. - if(theCSSPrefix) { - style[theCSSPrefix + prop.slice(0,1).toUpperCase() + prop.slice(1)] = val; - } - - //Set unprefixed. - style[prop] = val; - } catch(ignore) {} - } - }; - - /** - * Cross browser event handling. - */ - var _addEvent = skrollr.addEvent = function(element, names, callback) { - var intermediate = function(e) { - //Normalize IE event stuff. - e = e || window.event; - - if(!e.target) { - e.target = e.srcElement; - } - - if(!e.preventDefault) { - e.preventDefault = function() { - e.returnValue = false; - e.defaultPrevented = true; - }; - } - - return callback.call(this, e); - }; - - names = names.split(' '); - - var name; - var nameCounter = 0; - var namesLength = names.length; - - for(; nameCounter < namesLength; nameCounter++) { - name = names[nameCounter]; - - if(element.addEventListener) { - element.addEventListener(name, callback, false); - } else { - element.attachEvent('on' + name, intermediate); - } - - //Remember the events to be able to flush them later. - _registeredEvents.push({ - element: element, - name: name, - listener: callback - }); - } - }; - - var _removeEvent = skrollr.removeEvent = function(element, names, callback) { - names = names.split(' '); - - var nameCounter = 0; - var namesLength = names.length; - - for(; nameCounter < namesLength; nameCounter++) { - if(element.removeEventListener) { - element.removeEventListener(names[nameCounter], callback, false); - } else { - element.detachEvent('on' + names[nameCounter], callback); - } - } - }; - - var _removeAllEvents = function() { - var eventData; - var eventCounter = 0; - var eventsLength = _registeredEvents.length; - - for(; eventCounter < eventsLength; eventCounter++) { - eventData = _registeredEvents[eventCounter]; - - _removeEvent(eventData.element, eventData.name, eventData.listener); - } - - _registeredEvents = []; - }; - - var _emitEvent = function(element, name, direction) { - if(_listeners.keyframe) { - _listeners.keyframe.call(_instance, element, name, direction); - } - }; - - var _reflow = function() { - var pos = _instance.getScrollTop(); - - //Will be recalculated by _updateDependentKeyFrames. - _maxKeyFrame = 0; - - if(_forceHeight && !_isMobile) { - //un-"force" the height to not mess with the calculations in _updateDependentKeyFrames (#216). - body.style.height = ''; - } - - _updateDependentKeyFrames(); - - if(_forceHeight && !_isMobile) { - //"force" the height. - body.style.height = (_maxKeyFrame + documentElement.clientHeight) + 'px'; - } - - //The scroll offset may now be larger than needed (on desktop the browser/os prevents scrolling farther than the bottom). - if(_isMobile) { - _instance.setScrollTop(Math.min(_instance.getScrollTop(), _maxKeyFrame)); - } else { - //Remember and reset the scroll pos (#217). - _instance.setScrollTop(pos, true); - } - - _forceRender = true; - }; - - /* - * Returns a copy of the constants object where all functions and strings have been evaluated. - */ - var _processConstants = function() { - var viewportHeight = documentElement.clientHeight; - var copy = {}; - var prop; - var value; - - for(prop in _constants) { - value = _constants[prop]; - - if(typeof value === 'function') { - value = value.call(_instance); - } - //Percentage offset. - else if((/p$/).test(value)) { - value = (value.slice(0, -1) / 100) * viewportHeight; - } - - copy[prop] = value; - } - - return copy; - }; - - /* - * Returns the height of the document. - */ - var _getDocumentHeight = function() { - var skrollrBodyHeight = (_skrollrBody && _skrollrBody.offsetHeight || 0); - var bodyHeight = Math.max(skrollrBodyHeight, body.scrollHeight, body.offsetHeight, documentElement.scrollHeight, documentElement.offsetHeight, documentElement.clientHeight); - - return bodyHeight - documentElement.clientHeight; - }; - - /** - * Returns a string of space separated classnames for the current element. - * Works with SVG as well. - */ - var _getClass = function(element) { - var prop = 'className'; - - //SVG support by using className.baseVal instead of just className. - if(window.SVGElement && element instanceof window.SVGElement) { - element = element[prop]; - prop = 'baseVal'; - } - - return element[prop]; - }; - - /** - * Adds and removes a CSS classes. - * Works with SVG as well. - * add and remove are arrays of strings, - * or if remove is ommited add is a string and overwrites all classes. - */ - var _updateClass = function(element, add, remove) { - var prop = 'className'; - - //SVG support by using className.baseVal instead of just className. - if(window.SVGElement && element instanceof window.SVGElement) { - element = element[prop]; - prop = 'baseVal'; - } - - //When remove is ommited, we want to overwrite/set the classes. - if(remove === undefined) { - element[prop] = add; - return; - } - - //Cache current classes. We will work on a string before passing back to DOM. - var val = element[prop]; - - //All classes to be removed. - var classRemoveIndex = 0; - var removeLength = remove.length; - - for(; classRemoveIndex < removeLength; classRemoveIndex++) { - val = _untrim(val).replace(_untrim(remove[classRemoveIndex]), ' '); - } - - val = _trim(val); - - //All classes to be added. - var classAddIndex = 0; - var addLength = add.length; - - for(; classAddIndex < addLength; classAddIndex++) { - //Only add if el not already has class. - if(_untrim(val).indexOf(_untrim(add[classAddIndex])) === -1) { - val += ' ' + add[classAddIndex]; - } - } - - element[prop] = _trim(val); - }; - - var _trim = function(a) { - return a.replace(rxTrim, ''); - }; - - /** - * Adds a space before and after the string. - */ - var _untrim = function(a) { - return ' ' + a + ' '; - }; - - var _now = Date.now || function() { - return +new Date(); - }; - - var _keyFrameComparator = function(a, b) { - return a.frame - b.frame; - }; - - /* - * Private variables. - */ - - //Singleton - var _instance; - - /* - A list of all elements which should be animated associated with their the metadata. - Exmaple skrollable with two key frames animating from 100px width to 20px: - - skrollable = { - element: , - styleAttr: