You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

wow.coffee 8.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. #
  2. # Name : wow
  3. # Author : Matthieu Aussaguel, http://mynameismatthieu.com/, @mattaussaguel
  4. # Version : 1.1.2
  5. # Repo : https://github.com/matthieua/WOW
  6. # Website : http://mynameismatthieu.com/wow
  7. #
  8. class Util
  9. extend: (custom, defaults) ->
  10. custom[key] ?= value for key, value of defaults
  11. custom
  12. isMobile: (agent) ->
  13. /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(agent)
  14. createEvent: (event, bubble = false, cancel = false, detail = null) ->
  15. if document.createEvent? # W3C DOM
  16. customEvent = document.createEvent('CustomEvent')
  17. customEvent.initCustomEvent(event, bubble, cancel, detail)
  18. else if document.createEventObject? # IE DOM < 9
  19. customEvent = document.createEventObject()
  20. customEvent.eventType = event
  21. else
  22. customEvent.eventName = event
  23. customEvent
  24. emitEvent: (elem, event) ->
  25. if elem.dispatchEvent? # W3C DOM
  26. elem.dispatchEvent(event)
  27. else if event of elem?
  28. elem[event]()
  29. else if "on#{event}" of elem?
  30. elem["on#{event}"]()
  31. addEvent: (elem, event, fn) ->
  32. if elem.addEventListener? # W3C DOM
  33. elem.addEventListener event, fn, false
  34. else if elem.attachEvent? # IE DOM
  35. elem.attachEvent "on#{event}", fn
  36. else # fallback
  37. elem[event] = fn
  38. removeEvent: (elem, event, fn) ->
  39. if elem.removeEventListener? # W3C DOM
  40. elem.removeEventListener event, fn, false
  41. else if elem.detachEvent? # IE DOM
  42. elem.detachEvent "on#{event}", fn
  43. else # fallback
  44. delete elem[event]
  45. innerHeight: ->
  46. if 'innerHeight' of window
  47. window.innerHeight
  48. else document.documentElement.clientHeight
  49. # Minimalistic WeakMap shim, just in case.
  50. WeakMap = @WeakMap or @MozWeakMap or \
  51. class WeakMap
  52. constructor: ->
  53. @keys = []
  54. @values = []
  55. get: (key) ->
  56. for item, i in @keys
  57. if item is key
  58. return @values[i]
  59. set: (key, value) ->
  60. for item, i in @keys
  61. if item is key
  62. @values[i] = value
  63. return
  64. @keys.push(key)
  65. @values.push(value)
  66. # Dummy MutationObserver, to avoid raising exceptions.
  67. MutationObserver = @MutationObserver or @WebkitMutationObserver or @MozMutationObserver or \
  68. class MutationObserver
  69. constructor: ->
  70. console?.warn 'MutationObserver is not supported by your browser.'
  71. console?.warn 'WOW.js cannot detect dom mutations, please call .sync() after loading new content.'
  72. @notSupported: true
  73. observe: ->
  74. # getComputedStyle shim, from http://stackoverflow.com/a/21797294
  75. getComputedStyle = @getComputedStyle or \
  76. (el, pseudo) ->
  77. @getPropertyValue = (prop) ->
  78. prop = 'styleFloat' if prop is 'float'
  79. prop.replace(getComputedStyleRX, (_, _char)->
  80. _char.toUpperCase()
  81. ) if getComputedStyleRX.test prop
  82. el.currentStyle?[prop] or null
  83. @
  84. getComputedStyleRX = /(\-([a-z]){1})/g
  85. class @WOW
  86. defaults:
  87. boxClass: 'wow'
  88. animateClass: 'animated'
  89. offset: 0
  90. mobile: true
  91. live: true
  92. callback: null
  93. constructor: (options = {}) ->
  94. @scrolled = true
  95. @config = @util().extend(options, @defaults)
  96. # Map of elements to animation names:
  97. @animationNameCache = new WeakMap()
  98. @wowEvent = @util().createEvent(@config.boxClass)
  99. init: ->
  100. @element = window.document.documentElement
  101. if document.readyState in ["interactive", "complete"]
  102. @start()
  103. else
  104. @util().addEvent document, 'DOMContentLoaded', @start
  105. @finished = []
  106. start: =>
  107. @stopped = false
  108. @boxes = (box for box in @element.querySelectorAll(".#{@config.boxClass}"))
  109. @all = (box for box in @boxes)
  110. if @boxes.length
  111. if @disabled()
  112. @resetStyle()
  113. else
  114. @applyStyle(box, true) for box in @boxes
  115. if !@disabled()
  116. @util().addEvent window, 'scroll', @scrollHandler
  117. @util().addEvent window, 'resize', @scrollHandler
  118. @interval = setInterval @scrollCallback, 50
  119. if @config.live
  120. new MutationObserver (records) =>
  121. for record in records
  122. @doSync(node) for node in record.addedNodes or []
  123. .observe document.body,
  124. childList: true
  125. subtree: true
  126. # unbind the scroll event
  127. stop: ->
  128. @stopped = true
  129. @util().removeEvent window, 'scroll', @scrollHandler
  130. @util().removeEvent window, 'resize', @scrollHandler
  131. clearInterval @interval if @interval?
  132. sync: (element) ->
  133. @doSync(@element) if MutationObserver.notSupported
  134. doSync: (element) ->
  135. element ?= @element
  136. return unless element.nodeType is 1
  137. element = element.parentNode or element
  138. for box in element.querySelectorAll(".#{@config.boxClass}")
  139. unless box in @all
  140. @boxes.push box
  141. @all.push box
  142. if @stopped or @disabled()
  143. @resetStyle()
  144. else
  145. @applyStyle(box, true)
  146. @scrolled = true
  147. # show box element
  148. show: (box) ->
  149. @applyStyle(box)
  150. box.className = "#{box.className} #{@config.animateClass}"
  151. @config.callback(box) if @config.callback?
  152. @util().emitEvent(box, @wowEvent)
  153. @util().addEvent(box, 'animationend', @resetAnimation)
  154. @util().addEvent(box, 'oanimationend', @resetAnimation)
  155. @util().addEvent(box, 'webkitAnimationEnd', @resetAnimation)
  156. @util().addEvent(box, 'MSAnimationEnd', @resetAnimation)
  157. box
  158. applyStyle: (box, hidden) ->
  159. duration = box.getAttribute('data-wow-duration')
  160. delay = box.getAttribute('data-wow-delay')
  161. iteration = box.getAttribute('data-wow-iteration')
  162. @animate => @customStyle(box, hidden, duration, delay, iteration)
  163. animate: (->
  164. if 'requestAnimationFrame' of window
  165. (callback) ->
  166. window.requestAnimationFrame callback
  167. else
  168. (callback) ->
  169. callback()
  170. )()
  171. resetStyle: ->
  172. box.style.visibility = 'visible' for box in @boxes
  173. resetAnimation: (event) =>
  174. if event.type.toLowerCase().indexOf('animationend') >= 0
  175. target = event.target || event.srcElement
  176. target.className = target.className.replace(@config.animateClass, '').trim()
  177. customStyle: (box, hidden, duration, delay, iteration) ->
  178. @cacheAnimationName(box) if hidden
  179. box.style.visibility = if hidden then 'hidden' else 'visible'
  180. @vendorSet box.style, animationDuration: duration if duration
  181. @vendorSet box.style, animationDelay: delay if delay
  182. @vendorSet box.style, animationIterationCount: iteration if iteration
  183. @vendorSet box.style, animationName: if hidden then 'none' else @cachedAnimationName(box)
  184. box
  185. vendors: ["moz", "webkit"]
  186. vendorSet: (elem, properties) ->
  187. for name, value of properties
  188. elem["#{name}"] = value
  189. elem["#{vendor}#{name.charAt(0).toUpperCase()}#{name.substr 1}"] = value for vendor in @vendors
  190. vendorCSS: (elem, property) ->
  191. style = getComputedStyle(elem)
  192. result = style.getPropertyCSSValue(property)
  193. result = result or style.getPropertyCSSValue("-#{vendor}-#{property}") for vendor in @vendors
  194. result
  195. animationName: (box) ->
  196. try
  197. animationName = @vendorCSS(box, 'animation-name').cssText
  198. catch # Opera, fall back to plain property value
  199. animationName = getComputedStyle(box).getPropertyValue('animation-name')
  200. if animationName is 'none'
  201. '' # SVG/Firefox, unable to get animation name?
  202. else
  203. animationName
  204. cacheAnimationName: (box) ->
  205. # https://bugzilla.mozilla.org/show_bug.cgi?id=921834
  206. # box.dataset is not supported for SVG elements in Firefox
  207. @animationNameCache.set(box, @animationName(box))
  208. cachedAnimationName: (box) ->
  209. @animationNameCache.get(box)
  210. # fast window.scroll callback
  211. scrollHandler: =>
  212. @scrolled = true
  213. scrollCallback: =>
  214. if @scrolled
  215. @scrolled = false
  216. @boxes = for box in @boxes when box
  217. if @isVisible(box)
  218. @show(box)
  219. continue
  220. box
  221. @stop() unless @boxes.length or @config.live
  222. # Calculate element offset top
  223. offsetTop: (element) ->
  224. # SVG elements don't have an offsetTop in Firefox.
  225. # This will use their nearest parent that has an offsetTop.
  226. # Also, using ('offsetTop' of element) causes an exception in Firefox.
  227. element = element.parentNode while element.offsetTop is undefined
  228. top = element.offsetTop
  229. top += element.offsetTop while element = element.offsetParent
  230. top
  231. # check if box is visible
  232. isVisible: (box) ->
  233. offset = box.getAttribute('data-wow-offset') or @config.offset
  234. viewTop = window.pageYOffset
  235. viewBottom = viewTop + Math.min(@element.clientHeight, @util().innerHeight()) - offset
  236. top = @offsetTop(box)
  237. bottom = top + box.clientHeight
  238. top <= viewBottom and bottom >= viewTop
  239. util: ->
  240. @_util ?= new Util()
  241. disabled: ->
  242. not @config.mobile and @util().isMobile(navigator.userAgent)