diff --git a/Gruntfile.coffee b/Gruntfile.coffee index b499b1c..c6ecd4c 100755 --- a/Gruntfile.coffee +++ b/Gruntfile.coffee @@ -76,6 +76,20 @@ module.exports = (grunt) -> } } + # Compile Coffee for NPM bundle + coffee: { + main: { + files: { + 'dist/scroll-proxy.js': 'src/ScrollProxy.coffee' + 'dist/SUtil.js': 'src/SUtil.coffee' + } + } + options: { + bare: true + banner: '/*<%= pkg.name %>@<%= pkg.version %>*/' + } + } + # Run tests using Mocha locally mochify: { test: { @@ -174,6 +188,7 @@ module.exports = (grunt) -> grunt.loadNpmTasks('grunt-codo') grunt.loadNpmTasks('grunt-coffeelint') grunt.loadNpmTasks('grunt-contrib-clean') + grunt.loadNpmTasks('grunt-contrib-coffee') grunt.loadNpmTasks('grunt-mochify') grunt.loadNpmTasks('grunt-contrib-connect') grunt.loadNpmTasks('grunt-contrib-watch') @@ -194,6 +209,7 @@ module.exports = (grunt) -> ]) grunt.registerTask('build', [ 'test' + 'coffee' 'browserify:standalone' 'docs' ]) diff --git a/bower.json b/bower.json index 8dfaa23..97bf544 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "scroll-proxy", - "version": "0.2.0", + "version": "0.2.1", "authors": [ "Matheus Kautzmann " ], diff --git a/dist/SUtil.js b/dist/SUtil.js new file mode 100644 index 0000000..d8e1397 --- /dev/null +++ b/dist/SUtil.js @@ -0,0 +1,179 @@ + +/* + Helper class for ScrollProxy + @private + @since 0.1.0 + @author Matheus Kautzmann - + */ +var SUtil, + slice = [].slice; + +module.exports = SUtil = (function() { + function SUtil() {} + + + /* + Function that calls the callback with the given args and context + @param [Function] callback function that will be called back + @param [Object] context `this` context that will be applied to callback + @param [Array] args (Optional )A splat with the arguments that will be + passed along + @return [Null] it will return null if can't call the callback, undefined + otherwise + @example Calling func with ctx as this and no arguments: + SUtil.reportChange(func, ctx) + @example Calling the func with ctx as this and two arguments: + SUtil.reportChange(func, ctx, 1, 2) + @since 0.1.0 + */ + + SUtil.reportChange = function() { + var args, callback, context; + callback = arguments[0], context = arguments[1], args = 3 <= arguments.length ? slice.call(arguments, 2) : []; + if (SUtil.isFunction(callback)) { + return callback.apply(context, args); + } + return null; + }; + + + /* + Reliably gets the JS type of the given variable + @see http://bonsaiden.github.io/JavaScript-Garden/#types.typeof + @example Checking a string will return [object String] + SUtil.getType('hello') + @example Checking a number will return [object Number] + SUtil.getType(1) + @param [Object] target variable to be type tested + @return [String] The type of the given variable + @since 0.1.0 + */ + + SUtil.getType = function(target) { + return Object.prototype.toString.call(target); + }; + + + /* + Removes an item from an array, changing the array involved and returning + the element removed + @example Removing the first element of the array + SUtil.removeItemFromArray([1, 2], 0) + @param [Array] arr Array to remove the element from + @param [Number] idx Index of the element to be removed + @return [Any] the element removed + @since 0.1.0 + */ + + SUtil.removeItemFromArray = function(arr, idx) { + return arr.splice(idx, 1)[0]; + }; + + + /* + Checks if the suplied variable is a valid JS Function + @example Checking a valid function will return true + SUtil.isFunction(-> null) + @example Checking a number will return false + SUtil.isFunction(1) + @param [Function] func Variable to be tested as function + @return [Boolean] true if function and false if not + @since 0.1.0 + */ + + SUtil.isFunction = function(func) { + return SUtil.getType(func) === '[object Function]'; + }; + + + /* + Checks if the suplied variable is a valid JS Number + @example Checking a number will return true + SUtil.isNumber(1) + @example Checking a bool will return false + SUtil.isNumber(true) + @param [Number] number Variable to be tested as number + @return [Boolean] true if number and false if not + @since 0.1.0 + */ + + SUtil.isNumber = function(number) { + return SUtil.getType(number) === '[object Number]'; + }; + + + /* + Checks if the suplied variable is a valid JS String + @example Checking a string will return true + SUtil.isString('hello') + @example Checking a number will return false + SUtil.isString(1) + @param [Number] number Variable to be tested as string + @return [Boolean] true if string and false if not + @since 0.1.0 + */ + + SUtil.isString = function(string) { + return SUtil.getType(string) === '[object String]'; + }; + + + /* + Checks if the suplied variable can be used as event name for ScrollProxy + @example Empty strings will return false + SUtil.isValidEventName('') + @example Non-string object will return false + SUtil.isValidEventName(1) + @example String with one or more chars will return true + SUtil.isValidEventName('myEvent') + @param [String name Variable to be tested as valid event name + @return [Boolean] true if valid name and false if not + @since 0.1.0 + */ + + SUtil.isValidEventName = function(name) { + return SUtil.isString(name) && name !== ''; + }; + + + /* + Checks if the suplied variable is a positive number, it also considers 0 + a positive number + @example Checking for numbers greater than zero will return true + SUtil.isPositiveNumber(1) + @example Checking for 0 itself will return true + SUtil.isPositiveNumber(0) + @example Checking for number lower than 0 will return false + SUtil.isPositiveNumber(-1) + @param [String name Variable to be tested as positive number + @return [Boolean] true if positive number and false if not + @since 0.1.0 + */ + + SUtil.isPositiveNumber = function(number) { + return SUtil.isNumber(number) && number >= 0; + }; + + + /* + Checks if the suplied variable is a negative number, it does not + considers 0 a negative number + @example Checking for anything larger than zero will return false + SUtil.isNegativeNumber(1) + @example Checking for 0 itself will return false since it is considered + positive + SUtil.isNegativeNumber(0) + @example Numbers lower than zero will return true + SUtil.isNegativeNumber(-1) + @param [String name Variable to be tested as negative number + @return [Boolean] true if negative number and false if not + @since 0.1.0 + */ + + SUtil.isNegativeNumber = function(number) { + return SUtil.isNumber(number) && number < 0; + }; + + return SUtil; + +})(); diff --git a/dist/scroll-proxy.js b/dist/scroll-proxy.js new file mode 100644 index 0000000..eda8245 --- /dev/null +++ b/dist/scroll-proxy.js @@ -0,0 +1,1078 @@ +var SUtil, ScrollProxy, + indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + +SUtil = require('./SUtil'); + + +/* + Main class, containing the scrolling related events and API + + @option _targetList [HTMLElement] target The anchor of the ScrollProxy + instance + @option _targetList [ScrollProxy] proxy The instance of ScrollProxy + + @option _events [String] type The event name of the event + @option _events [Function] fn The function that will handle the callback + @option _events [Number] timeout The timeout id of the throttled **fn** + + @since 0.1.0 + @author Matheus Kautzmann - + */ + +module.exports = ScrollProxy = (function() { + + /* + @property [Boolean] Class variable that indicates if ScrollProxy is + currently enabled + */ + ScrollProxy.active = false; + + + /* + @property [Array] Private Class variable that holds + all the elements currently tracked by ScrollProxy + */ + + ScrollProxy._targetList = []; + + + /* + @property [String] Class constant that maps to the **scroll** event + name + */ + + ScrollProxy.SCROLL = 'scroll'; + + + /* + @property [String] Class constant that maps to the **offsetX** + event name + */ + + ScrollProxy.OFFSET_X = 'offsetX'; + + + /* + @property [String] Class constant that maps to the **offsetY** + event name + */ + + ScrollProxy.OFFSET_Y = 'offsetY'; + + + /* + @property [String] Class constant that maps to the **top** + event name + */ + + ScrollProxy.TOP = 'top'; + + + /* + @property [String] Class constant that maps to the **bottom** + event name + */ + + ScrollProxy.BOTTOM = 'bottom'; + + + /* + @property [String] Class constant that maps to the **left** + event name + */ + + ScrollProxy.LEFT = 'left'; + + + /* + @property [String] Class constant that maps to the **right** + event name + */ + + ScrollProxy.RIGHT = 'right'; + + + /* + @property [String] Class constant that maps to the **visible** + event name + */ + + ScrollProxy.VISIBLE = 'visible'; + + + /* + @property [String] Class constant that maps to the **invisible** + event name + */ + + ScrollProxy.INVISIBLE = 'invisible'; + + + /* + @property [String] Class constant that maps to the **disable-hover** + event name + */ + + ScrollProxy.DISABLE_HOVER = 'disable-hover'; + + + /* + @property [String] Private instance variable that maps to the + target's **x** position property + */ + + ScrollProxy.prototype._targetX = 'scrollLeft'; + + + /* + @property [String] Private instance variable that maps to the + target's **y** position property + */ + + ScrollProxy.prototype._targetY = 'scrollTop'; + + + /* + @property [HTMLElement] Private instance variable that indicates the target + this ScrollProxy's instance is attached to + */ + + ScrollProxy.prototype._target = null; + + + /* + @property [Array] Private instance variable that holds the events + this instance responds to + */ + + ScrollProxy.prototype._events = []; + + + /* + @property [Number] Private instance variable that holds the + current delay for scroll throttling + */ + + ScrollProxy.prototype._scrollDelay = 250; + + + /* + @property [Number] Private instance variable that holds the + current delay for enabling/disabling hover elements on scroll + */ + + ScrollProxy.prototype._hoverDelay = 250; + + + /* + @property [Number] Private instance variable that holds the + current timeout used when blocking hover animations + */ + + ScrollProxy.prototype._hoverTimeout = null; + + + /* + @property [Number] Instance variable that holds the current x position of + the scroll target + */ + + ScrollProxy.prototype.x = 0; + + + /* + @property [Number] Instance variable that holds the current y position of + the scroll target + */ + + ScrollProxy.prototype.y = 0; + + + /* + @property [Number] Instance variable that holds the current width of + the scroll target box + */ + + ScrollProxy.prototype.width = 0; + + + /* + @property [Number] Instance variable that holds the current height of + the scroll target box + */ + + ScrollProxy.prototype.height = 0; + + + /* + @property [Number] scrollWidth Instance variable that holds the current + width of the scroll target scroll area + */ + + ScrollProxy.prototype.scrollWidth = 0; + + + /* + @property [Number] Instance variable that holds the current + height of the scroll target scroll area + */ + + ScrollProxy.prototype.scrollHeight = 0; + + + /* + @property [Object] Instance variable that holds the current + scroll data (from last scroll action) + */ + + ScrollProxy.prototype.scrollData = {}; + + + /* + Handles the scroll event when it fires + @example Simulating the window firing an event passing the metadata: + window.addEventListener('scroll', ScrollProxy.handleScroll, true) + @param [Object] e scroll event data + @return [Object] Null or the element that catched the event + @since 0.1.0 + @private + */ + + ScrollProxy._handleScroll = function(e) { + var len, ref, target; + len = ScrollProxy._targetList.length; + if (len === 0) { + return null; + } + target = e.target.body || e.target; + return (ref = ScrollProxy._getElement(target)) != null ? ref.proxy._performScroll(e) : void 0; + }; + + + /* + Retrieves the current target and proxy for the given HTMLElement + @example Checking if the body tag is on _targetList: + ScrollProxy._getElement(document.body) + @param [HTMLElement] el The element to search for in **_targetList** + @return [Object] Null or the target that matched the argument + @since 0.1.0 + @private + */ + + ScrollProxy._getElement = function(el) { + var item, j, len1, ref; + ref = ScrollProxy._targetList; + for (j = 0, len1 = ref.length; j < len1; j++) { + item = ref[j]; + if ((item != null) && item.target === el) { + return item; + } + } + return null; + }; + + + /* + Activates the ScrollProxy's functionality, registering the event listener + that will listen for scroll events and proxy it to + **ScrollProxy.handleScroll** + @example Activating ScrollProxy manually, it's done automatically with new: + ScrollProxy.activate() + @return [Boolean] The current status of ScrollProxy + @since 0.1.0 + */ + + ScrollProxy.activate = function() { + if (!ScrollProxy.active) { + window.addEventListener(ScrollProxy.SCROLL, ScrollProxy._handleScroll, true); + ScrollProxy.active = true; + } + return ScrollProxy.active; + }; + + + /* + Deactivates the ScrollProxy's functionality, removing the event listener and + stopping any functionality + @example Deactivating manually, it's done automatically when unregistering: + ScrollProxy.deactivate() + @return [Boolean] The current status of ScrollProxy + @since 0.1.0 + */ + + ScrollProxy.deactivate = function() { + window.removeEventListener(ScrollProxy.SCROLL, ScrollProxy._handleScroll, true); + return ScrollProxy.active = false; + }; + + + /* + Cleans the **_targetList**, removing all targets + @example To assure ScrollProxy is shut down you could run **clean**: + ScrollProxy.clean() + @return [Array] Targets removed + @since 0.1.0 + */ + + ScrollProxy.clean = function() { + ScrollProxy.deactivate(); + return ScrollProxy._targetList = []; + }; + + + /* + Update this instance rect information, updating x and y positions as well as + width, height, scrollWidth and scrollHeight + @example Most methods run this function when up-to-date data is required: + s = new ScrollProxy() + document.body.scrollTop = 20 # s.y continues 0 + s.updateViewportInfo() # s.y is 20 now + @return [Null] Nothing useful is returned + @since 0.1.0 + @private + */ + + ScrollProxy.prototype._updateViewportInfo = function() { + this.x = this._target[this._targetX]; + this.y = this._target[this._targetY]; + this.width = this._target.offsetWidth; + this.height = this._target.offsetHeight; + this.scrollWidth = this._target.scrollWidth; + this.scrollHeight = this._target.scrollHeight; + return null; + }; + + + /* + Utility method that Throws error if element given is not child of the + **_target** + @example Checking a list item inside a ul: + s = new ScrollProxy(ul) + s._checkElement(li) # does not throw anything, so OK + @param [HTMLElement] el The element to check + @return [Null] Nothing useful is returned + @since 0.1.0 + @private + */ + + ScrollProxy.prototype._checkElement = function(el) { + if (indexOf.call(this._target.children, el) < 0) { + throw new Error('Element must be child of the scroll element'); + } + return null; + }; + + + /* + Remove an event from the list of events + @example Don't want to receive scroll feedback anymore: + s = new ScrollProxy() + evt = s._createEvent('scroll', -> null) + s._removeEvent(evt) # OK, event gone + @param [Object] The event to remove + @return [Array] The new array of events (**_events**) + @since 0.1.0 + @private + */ + + ScrollProxy.prototype._removeEvent = function(event) { + var arr, i, item, j, k, len1, len2, ref, ref1; + ref = this._events; + for (i = j = 0, len1 = ref.length; j < len1; i = ++j) { + item = ref[i]; + if (event === item) { + window.clearTimeout(event.timeout); + delete event.fn; + delete this._events[i]; + break; + } + } + arr = []; + ref1 = this._events; + for (k = 0, len2 = ref1.length; k < len2; k++) { + item = ref1[k]; + if (item != null) { + arr.push(item); + } + } + return this._events = arr; + }; + + + /* + Checks if the given virtual rect is visible on **_target** viewport + @example Check if the rect (0, 0, 50, 50) is visible: + s = new ScrollProxy() + rect = {x: 0, y:0, w: 50, h: 50} + s._isRectVisible(rect) # Visible? + @param [Object] rect The rect object to check + @option rect [Number] x The top-left x position + @option rect [Number] y The top-left y position + @option rect [Number] w The width of the rect + @option rect [Number] h The height of the rect + @return [Boolean] Indicates if visible or not + @since 0.1.0 + @private + */ + + ScrollProxy.prototype._isRectVisible = function(rect) { + var xVisible, yVisible; + this._updateViewportInfo(); + xVisible = this.x < (rect.x + rect.w) && this.x > (rect.x - this.width); + yVisible = this.y < (rect.y + rect.h) && this.y > (rect.y - this.height); + return xVisible && yVisible; + }; + + + /* + Performs the scroll actions, firing the needed events internally + @example The **_handleScroll** usually calls this method: + s = new ScrollProxy() + s._performScroll(scrollEvt) # OK, related events will be fired + @param [Object] The scroll data passed with the event + @return [HTMLElement] The current target + @since 0.1.0 + @private + */ + + ScrollProxy.prototype._performScroll = function(e) { + var event, j, len, len1, ref; + len = this._events.length; + if (len === 0) { + return null; + } + this.scrollData = e; + ref = this._events; + for (j = 0, len1 = ref.length; j < len1; j++) { + event = ref[j]; + event.fn.call(this, event); + } + return this._target; + }; + + + /* + Function that performs the scroll throttling to achieve better performance + @example One must pass a function to be throttled and a delay: + s = new ScrollProxy() + func = -> this + newFunc = s._throttleScroll(func, 250) + newFunc() # func will be called after 250ms, the scrollData will be passed + newFunc() # func will not be called, because other call is waiting + @param [Function] The function to throttle + @param [Number] delay The time function should wait to fire + @return [Function] The new throttled function + @since 0.1.0 + @private + */ + + ScrollProxy.prototype._throttleScroll = function(func, delay) { + var running; + running = false; + return (function(_this) { + return function(data) { + if (running) { + return; + } + running = true; + return data.timeout = window.setTimeout(function() { + running = false; + return SUtil.reportChange(func, _this, _this.scrollData); + }, (delay != null ? delay : _this.getScrollDelay())); + }; + })(this); + }; + + + /* + Creates a generic event and pushes it in the **_events** array + @example Create event with name, callback and optional throttle delay: + s = new ScrollProxy() + s._createEvent('scroll', -> null, 250) # setting delay of 250ms + @param [String] evt The event name to register + @param [Function] callback the function to be called when event is fired + @param [Function] origin the original function passed, `off` searches for it + @param [Number] delay Time to wait in milliseconds + @return [Object] The event just registered + @since 0.2.0 + @private + */ + + ScrollProxy.prototype._createEvent = function(evt, callback, origin, delay) { + var fn; + fn = this._throttleScroll(callback, delay); + return this._events.push({ + type: evt, + fn: fn, + origin: origin, + timeout: null + }); + }; + + + /* + Creates a scroll event + @example Create scroll event with callback and optional throttle delay: + s = new ScrollProxy() + s._createScrollEvent('scroll', -> null, 250) # setting delay of 250ms + @param [String] evt The event name to register (**scroll**) + @param [Function] callback the function to be called when event is fired + @param [Function] origin the original function passed, `off` searches for it + @param [Number] delay Time to wait in milliseconds + @return [Object] The event just registered + @since 0.2.0 + @private + */ + + ScrollProxy.prototype._createScrollEvent = function(evt, callback, origin, delay) { + if (origin == null) { + origin = callback; + } + return this._createEvent(evt, (function(_this) { + return function() { + _this._updateViewportInfo(); + return SUtil.reportChange(callback, _this, _this.scrollData); + }; + })(this), origin, delay); + }; + + + /* + Creates a offset event + @example Create offset event with callback and optional offset: + s = new ScrollProxy() + s._createOffsetEvent('offsetX', -> null, 250) # setting offset of 250px + @param [String] evt The event name to register, **offsetX** or **offsetY** + @param [Function] callback the function to be called when event is fired + @param [Number] offset Offset to check for (0 is default) + @return [Object] The event just registered + @since 0.2.0 + @private + */ + + ScrollProxy.prototype._createOffsetEvent = function(evt, callback, offset) { + if (offset == null) { + offset = 0; + } + if (evt === ScrollProxy.OFFSET_X) { + return this._createEvent(evt, (function(_this) { + return function() { + if (_this.x === _this._target[_this._targetX]) { + return; + } else { + _this._updateViewportInfo(); + } + if (SUtil.isPositiveNumber(offset) && _this.x < offset) { + return; + } + if (SUtil.isNegativeNumber(offset) && _this.x > Math.abs(offset)) { + return; + } + return SUtil.reportChange(callback, _this, _this.scrollData); + }; + })(this), callback); + } else { + return this._createEvent(evt, (function(_this) { + return function() { + if (_this.y === _this._target[_this._targetY]) { + return _this._updateViewportInfo(); + } else { + _this._updateViewportInfo(); + } + if (SUtil.isPositiveNumber(offset) && _this.y < offset) { + return; + } + if (SUtil.isNegativeNumber(offset) && _this.y > Math.abs(offset)) { + return; + } + return SUtil.reportChange(callback, _this, _this.scrollData); + }; + })(this), callback); + } + }; + + + /* + Creates a event to check for horizontal bound collision + @example Create horizontal bound event with callback and optional offset: + s = new ScrollProxy() + s._createHorizontalBoundEvent('left', -> null) + * One can also check with a offset inwards, useful for infinite scrolling + s._createHorizontalBoundEvent('left', -> null, 250) # fire 250px to left + @param [String] evt The event name to register, **left** or **right** + @param [Function] callback the function to be called when event is fired + @param [Number] offset Offset to check for (0 is defaut) + @return [Object] The event just registered + @since 0.2.0 + @private + */ + + ScrollProxy.prototype._createHorizontalBoundScrollEvent = function(evt, callback, offset) { + if (offset == null) { + offset = 0; + } + if (evt === ScrollProxy.LEFT) { + return this._createScrollEvent(evt, (function(_this) { + return function() { + if (_this.x <= Math.abs(offset)) { + return SUtil.reportChange(callback, _this, _this.scrollData); + } + }; + })(this), callback, 0); + } else { + return this._createScrollEvent(evt, (function(_this) { + return function() { + if ((_this.scrollWidth - _this.x) - _this.width <= Math.abs(offset)) { + return SUtil.reportChange(callback, _this, _this.scrollData); + } + }; + })(this), callback, 0); + } + }; + + + /* + Creates a event to check for vertical bound collision + @example Create vertical bound event with callback and optional offset: + s = new ScrollProxy() + s._createVerticalBoundEvent('top', -> null) + * One can also check with a offset inwards, useful for infinite scrolling + s._createVerticalBoundEvent('top', -> null, 250) # fire 250px to top + @param [String] evt The event name to register, **top** or **bottom** + @param [Function] callback the function to be called when event is fired + @param [Number] offset Offset to check for + @return [Object] The event just registered + @since 0.2.0 + @private + */ + + ScrollProxy.prototype._createVerticalBoundScrollEvent = function(evt, callback, offset) { + if (offset == null) { + offset = 0; + } + if (evt === ScrollProxy.TOP) { + return this._createScrollEvent(evt, (function(_this) { + return function() { + if (_this.y <= Math.abs(offset)) { + return SUtil.reportChange(callback, _this, _this.scrollData); + } + }; + })(this), callback, 0); + } else { + return this._createScrollEvent(evt, (function(_this) { + return function() { + if ((_this.scrollHeight - _this.y) - _this.height <= Math.abs(offset)) { + return SUtil.reportChange(callback, _this, _this.scrollData); + } + }; + })(this), callback, 0); + } + }; + + + /* + Creates a event to check for element visibility as one scrolls + @example Create visibility scroll event with callback and optional offset: + s = new ScrollProxy() + ul = document.querySelector('.myBeautifulList') + s._createVisibilityScrollEvent('visible', -> null, ul) + @param [String] evt The event name to register, **visible** or **invisible** + @param [Function] callback the function to be called when event is fired + @param [Number] offset Offset to check for + @return [Object] The event just registered + @since 0.2.0 + @private + */ + + ScrollProxy.prototype._createVisibilityScrollEvent = function(evt, callback, el) { + this._checkElement(el); + if (evt === ScrollProxy.VISIBLE) { + return this._createScrollEvent(evt, (function(_this) { + return function() { + if (_this.isElementVisible(el)) { + return SUtil.reportChange(callback, _this, _this.scrollData); + } + }; + })(this), callback); + } else { + return this._createScrollEvent(evt, (function(_this) { + return function() { + if (!_this.isElementVisible(el)) { + return SUtil.reportChange(callback, _this, _this.scrollData); + } + }; + })(this), callback); + } + }; + + + /* + The ScrollProxy constructor, use it to create new instances + @example Create a new ScrollProxy instance: + * Creating a instance in the document.body + s = new ScrollProxy() # it defaults to document.body + * Creating on a div + s = new ScrollProxy(myDiv) # or any HTMLEment + * Creating on window + s = new ScrollProxy(window) # Remaps to window.document.body + @param [HTMLElement] target The element to attach ScrollProxy + @return [ScrollProxy] This instance + @since 0.2.0 + */ + + function ScrollProxy(target) { + if (target == null) { + target = document; + } + if (target === document) { + target = target.body; + } + if (target === window) { + target = target.document.body; + } + if (target instanceof HTMLElement) { + this._target = target; + } else { + throw new Error('Could not attach to a valid anchor'); + } + this._events = []; + this.register(); + this._updateViewportInfo(); + return this; + } + + + /* + Sets the scroll delay, any event registered will respect the new value + @example Changing the scroll delay mid-flight: + s = new ScrollProxy() + * Any call will the throttled with 250ms + s.on('scroll', -> null) # defaults to 250ms delay + s.setScrollDelay(300) # any call now throttles with 300ms + @param [Number] ms The delay in milliseconds + @return [ScrollProxy] This instance + @since 0.1.0 + */ + + ScrollProxy.prototype.setScrollDelay = function(ms) { + if (SUtil.isPositiveNumber(ms)) { + this._scrollDelay = Math.round(ms); + } + return this; + }; + + + /* + Returns the current scroll delay + @example Getting the current delay: + s = new ScrollProxy() + s.setScrollDelay(300) # any call now throttles with 300ms + s.getScrollDelay() # will be 300 + @return [Number] The current value of _scrollDelay + @since 0.1.0 + */ + + ScrollProxy.prototype.getScrollDelay = function() { + return this._scrollDelay; + }; + + + /* + Sets the hover delay. The disable hover function will respect it + @example Changing the hover delay mid-flight: + s = new ScrollProxy() + * Hover animations have 250ms delay on scroll end by default + s.disableHoverOnScroll() + s.setHoverDelay(300) # now hover delay is 300ms + @param [Number] ms The delay in milliseconds + @return [ScrollProxy] This instance + @since 0.1.0 + */ + + ScrollProxy.prototype.setHoverDelay = function(ms) { + if (SUtil.isPositiveNumber(ms)) { + this._hoverDelay = Math.round(ms); + } + return this; + }; + + + /* + Returns the current hover delay + @example Getting the current delay: + s = new ScrollProxy() + s.setHoverDelay(300) # hover animations now have 300ms delay on scroll end + s.getHoverDelay() # will be 300 + @return [Number] The current value of _hoverDelay + @since 0.1.0 + */ + + ScrollProxy.prototype.getHoverDelay = function() { + return this._hoverDelay; + }; + + + /* + Returns the current target + @example Getting the current target: + s = new ScrollProxy(myDiv) + s.getTarget() # will equal myDiv + @return [HTMLElement] The current target + @since 0.1.0 + */ + + ScrollProxy.prototype.getTarget = function() { + return this._target; + }; + + + /* + Returns the rect for an HTMLElement + @example Getting body dimensions and position: + s = new ScrollProxy(myDiv) + s.getRect(document.body) + @param [HTMLElement] el The element to get the rect from + @return [Object] The rect + @since 0.1.0 + */ + + ScrollProxy.prototype.getRect = function(el) { + return { + x: el.offsetLeft, + y: el.offsetTop, + w: el.getBoundingClientRect().width, + h: el.getBoundingClientRect().height + }; + }; + + + /* + Register events in ScrollProxy + @example Registering a new scroll event on body: + s = new ScrollProxy() + s.on('scroll', -> 'Awesome scrolling!') + @param [String] evt The event name to Register + @param [Function] func The function to callback once event is fired + @param [Object] option A wildcard argument that is passed to the event + @event scroll The scroll event that is fired whenever one scrolls + @event offsetX The offset event on the X axis that fires once one scrolls + past the given horizontal offset + @event offsetY The offset event on the Y axis that fires once one scrolls + past the given vertical offset + @event top The event that fires once one reaches the top of the scroll area + or the given offset top + @event bottom The event that fires once one reaches the bottom of the scroll + area or the given offset bottom + @event left The event that fires once one reaches the left bound of the + scroll area of the given offset left + @event right The event that fires once one reaches the right bound of the + scroll area of the given offset right + @event visible The event that fires once the given element is visible on the + scroll area + @event invisible The event that fires once the given element is not visible + on the scroll area + @return [ScrollProxy] This instance + @since 0.1.0 + */ + + ScrollProxy.prototype.on = function(evt, func, option) { + if (!SUtil.isValidEventName(evt)) { + return this; + } + if (!SUtil.isFunction(func)) { + return this; + } + switch (evt) { + case ScrollProxy.SCROLL: + this._createScrollEvent(evt, func); + break; + case ScrollProxy.OFFSET_X: + case ScrollProxy.OFFSET_Y: + this._createOffsetEvent(evt, func, option); + break; + case ScrollProxy.TOP: + case ScrollProxy.BOTTOM: + this._createVerticalBoundScrollEvent(evt, func, option); + break; + case ScrollProxy.LEFT: + case ScrollProxy.RIGHT: + this._createHorizontalBoundScrollEvent(evt, func, option); + break; + case ScrollProxy.VISIBLE: + case ScrollProxy.INVISIBLE: + this._createVisibilityScrollEvent(evt, func, option); + } + return this; + }; + + + /* + Registers events with **on** but just fire the callback the first time + @example Register an one time event: + s = new ScrollProxy() + s.once('top', -> 'I AM ON TOP!') # fires just one time + @param [String] evt The event name to register + @param [Function] func The function to callback + @param [Object] option The wildcard argument that goes with events + @return [ScrollProxy] This instance + @since 0.2.0 + */ + + ScrollProxy.prototype.once = function(evt, func, option) { + var oneTimeFunction; + oneTimeFunction = (function(_this) { + return function() { + SUtil.reportChange(func, _this, _this.scrollData); + return _this.off(evt, oneTimeFunction); + }; + })(this); + return this.on(evt, oneTimeFunction, option); + }; + + + /* + Removes all events with the specified event name or name and function + @example Removing all handlers for one event: + s = new ScrollProxy() + s.on('scroll', -> 'Free scrolling') + s.off('scroll') # no more free scrolling + @example Removing just the specific function handler for the event + s = new ScrollProxy() + func = -> 'Free scrolling' + s.on('scroll', func) + s.off('scroll', func) + @param [String] evt The event name to register + @return [ScrollProxy] This instance + @since 0.2.0 + */ + + ScrollProxy.prototype.off = function(evt, func) { + var event, i, j, len1, ref; + if (!SUtil.isValidEventName(evt)) { + return this; + } + ref = this._events; + for (i = j = 0, len1 = ref.length; j < len1; i = ++j) { + event = ref[i]; + if ((func == null) && (event != null) && evt === event.type) { + this._removeEvent(event); + } else if ((func != null) && (event != null) && event.origin === func) { + this._removeEvent(event); + } + } + return this; + }; + + + /* + Register this target on ScrollProxy's **_targetList** + @example Registering a target (constructor does it automatically): + s = new ScrollProxy() + s.register() # Throws error because constructor already registered it + @example Unregistering first will work: + s = new ScrollProxy() + s.unregister() # Removing the target + s.register() # OK. Target already removed, adding it again + @return [ScrollProxy] This instance + @since 0.1.0 + */ + + ScrollProxy.prototype.register = function() { + var matchingTarget; + matchingTarget = ScrollProxy._getElement(this._target); + if (matchingTarget != null) { + throw new Error('Assign one proxy per target'); + } + ScrollProxy._targetList.push({ + target: this._target, + proxy: this + }); + ScrollProxy.activate(); + return this; + }; + + + /* + Unregister this target and remove it from ScrollProxy's **_targetList** + @example Unregistering target created automatically by the constructor: + s = new ScrollProxy() + s.unregister() # ScrollProxy now has no targets, so it deactivates + @return [ScrollProxy] This instance + @since 0.1.0 + */ + + ScrollProxy.prototype.unregister = function() { + var idx; + idx = ScrollProxy._targetList.length; + if (idx > 0) { + while (idx--) { + if (ScrollProxy._targetList[idx].proxy === this) { + SUtil.removeItemFromArray(ScrollProxy._targetList, idx); + break; + } + } + } + if (ScrollProxy._targetList.length === 0) { + ScrollProxy.deactivate(); + } + return this; + }; + + + /* + Checks if the element given is visible in the scroll area of the target + @example Checking if li is visible within parent ul's scroll area: + s = new ScrollProxy(ul) + s.isElementVisible(li) # Visible? May be true or false + @return [Boolean] Whether it's visible or not + @since 0.1.0 + */ + + ScrollProxy.prototype.isElementVisible = function(el) { + this._checkElement(el); + return this._isRectVisible(this.getRect(el)); + }; + + + /* + Disables hover animations on scrolling to improve scroll performance + @example It must be activated manually to avoid unexpected behavior: + s = new ScrollProxy() # defaults to document.body + s.disableHoverOnScroll() # Hover animations are now disabled on body + @return [ScrollProxy] This instance + @since 0.2.0 + */ + + ScrollProxy.prototype.disableHoverOnScroll = function() { + this._createScrollEvent(ScrollProxy.DISABLE_HOVER, (function(_this) { + return function() { + window.clearTimeout(_this._hoverTimeout); + _this._target.style.pointerEvents = 'none'; + return _this._hoverTimeout = window.setTimeout(function() { + return _this._target.style.pointerEvents = 'auto'; + }, _this.getHoverDelay()); + }; + })(this), null, 0); + return this; + }; + + + /* + Re-enables hover animations on scrolling + @example Once hover animations are disabled, you can re-enable with: + s = new ScrollProxy() # defaults to document.body + s.disableHoverOnScroll() # Hover animations on scrolling are now disabled + s.enableHoverOnScroll() # Hover animations on scrolling are now restored + @return [ScrollProxy] This instance + @since 0.1.0 + */ + + ScrollProxy.prototype.enableHoverOnScroll = function() { + this.off(ScrollProxy.DISABLE_HOVER); + this._target.style.pointerEvents = 'auto'; + return this; + }; + + return ScrollProxy; + +})(); diff --git a/dist/scroll-proxy.min.js b/dist/scroll-proxy.min.js index 5be8ab8..f8a051a 100644 --- a/dist/scroll-proxy.min.js +++ b/dist/scroll-proxy.min.js @@ -1,4 +1,4 @@ -/*scroll-proxy@0.2.0*/ +/*scroll-proxy@0.2.1*/ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.ScrollProxy = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o=0},t.isNegativeNumber=function(e){return t.isNumber(e)&&0>e},t}(); diff --git a/package.json b/package.json index a329b6e..a0ad635 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "scroll-proxy", - "version": "0.2.0", + "version": "0.2.1", "description": "Easy scroll handling in the browser. Zero dependencies. ~2kb gzipped", - "main": "dist/scroll-proxy.min.js", + "main": "dist/scroll-proxy.js", "scripts": { "test": "grunt test", "coverage": "node_modules/mocha-phantomjs/bin/mocha-phantomjs -R node_modules/mocha-lcov-reporter/lib/lcov.js test/index.html | sed -n '1!p' | sed 's,SF:,SF:src/,' | node_modules/coveralls/bin/coveralls.js" @@ -38,6 +38,7 @@ "grunt-codo": "~0.2.0", "grunt-coffeelint": "~0.0.13", "grunt-contrib-clean": "~0.6.0", + "grunt-contrib-coffee": "~0.13.0", "grunt-contrib-connect": "~0.11.2", "grunt-contrib-watch": "~0.6.1", "grunt-http-server": "~1.13.0",