Permalink
| function isIn(needle, haystack) { | |
| return haystack.indexOf(needle) >= 0; | |
| } | |
| function extend(custom, defaults) { | |
| for (const key in defaults) { | |
| if (custom[key] == null) { | |
| const value = defaults[key]; | |
| custom[key] = value; | |
| } | |
| } | |
| return custom; | |
| } | |
| function isMobile(agent) { | |
| return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(agent); | |
| } | |
| function createEvent(event, bubble = false, cancel = false, detail = null) { | |
| let customEvent; | |
| if (document.createEvent != null) { // W3C DOM | |
| customEvent = document.createEvent('CustomEvent'); | |
| customEvent.initCustomEvent(event, bubble, cancel, detail); | |
| } else if (document.createEventObject != null) { // IE DOM < 9 | |
| customEvent = document.createEventObject(); | |
| customEvent.eventType = event; | |
| } else { | |
| customEvent.eventName = event; | |
| } | |
| return customEvent; | |
| } | |
| function emitEvent(elem, event) { | |
| if (elem.dispatchEvent != null) { // W3C DOM | |
| elem.dispatchEvent(event); | |
| } else if (event in (elem != null)) { | |
| elem[event](); | |
| } else if (`on${event}` in (elem != null)) { | |
| elem[`on${event}`](); | |
| } | |
| } | |
| function addEvent(elem, event, fn) { | |
| if (elem.addEventListener != null) { // W3C DOM | |
| elem.addEventListener(event, fn, false); | |
| } else if (elem.attachEvent != null) { // IE DOM | |
| elem.attachEvent(`on${event}`, fn); | |
| } else { // fallback | |
| elem[event] = fn; | |
| } | |
| } | |
| function removeEvent(elem, event, fn) { | |
| if (elem.removeEventListener != null) { // W3C DOM | |
| elem.removeEventListener(event, fn, false); | |
| } else if (elem.detachEvent != null) { // IE DOM | |
| elem.detachEvent(`on${event}`, fn); | |
| } else { // fallback | |
| delete elem[event]; | |
| } | |
| } | |
| function getInnerHeight() { | |
| if ('innerHeight' in window) { | |
| return window.innerHeight; | |
| } | |
| return document.documentElement.clientHeight; | |
| } | |
| // Minimalistic WeakMap shim, just in case. | |
| const WeakMap = window.WeakMap || window.MozWeakMap || | |
| class WeakMap { | |
| constructor() { | |
| this.keys = []; | |
| this.values = []; | |
| } | |
| get(key) { | |
| for (let i = 0; i < this.keys.length; i++) { | |
| const item = this.keys[i]; | |
| if (item === key) { | |
| return this.values[i]; | |
| } | |
| } | |
| return undefined; | |
| } | |
| set(key, value) { | |
| for (let i = 0; i < this.keys.length; i++) { | |
| const item = this.keys[i]; | |
| if (item === key) { | |
| this.values[i] = value; | |
| return this; | |
| } | |
| } | |
| this.keys.push(key); | |
| this.values.push(value); | |
| return this; | |
| } | |
| }; | |
| // Dummy MutationObserver, to avoid raising exceptions. | |
| const MutationObserver = | |
| window.MutationObserver || window.WebkitMutationObserver || | |
| window.MozMutationObserver || | |
| class MutationObserver { | |
| constructor() { | |
| if (typeof console !== 'undefined' && console !== null) { | |
| console.warn('MutationObserver is not supported by your browser.'); | |
| console.warn( | |
| 'WOW.js cannot detect dom mutations, please call .sync() after loading new content.' | |
| ); | |
| } | |
| } | |
| static notSupported = true; | |
| observe() {} | |
| }; | |
| // getComputedStyle shim, from http://stackoverflow.com/a/21797294 | |
| const getComputedStyle = window.getComputedStyle || | |
| function getComputedStyle(el) { | |
| const getComputedStyleRX = /(\-([a-z]){1})/g; | |
| return { | |
| getPropertyValue(prop) { | |
| if (prop === 'float') { prop = 'styleFloat'; } | |
| if (getComputedStyleRX.test(prop)) { | |
| prop.replace(getComputedStyleRX, (_, _char) => _char.toUpperCase()); | |
| } | |
| const { currentStyle } = el; | |
| return (currentStyle != null ? currentStyle[prop] : void 0) || null; | |
| }, | |
| }; | |
| }; | |
| export default class WOW { | |
| defaults = { | |
| boxClass: 'wow', | |
| animateClass: 'animated', | |
| offset: 0, | |
| mobile: true, | |
| live: true, | |
| callback: null, | |
| scrollContainer: null, | |
| resetAnimation: true, | |
| }; | |
| constructor(options = {}) { | |
| this.start = this.start.bind(this); | |
| this.resetAnimation = this.resetAnimation.bind(this); | |
| this.scrollHandler = this.scrollHandler.bind(this); | |
| this.scrollCallback = this.scrollCallback.bind(this); | |
| this.scrolled = true; | |
| this.config = extend(options, this.defaults); | |
| if (options.scrollContainer != null) { | |
| this.config.scrollContainer = document.querySelector(options.scrollContainer); | |
| } | |
| // Map of elements to animation names: | |
| this.animationNameCache = new WeakMap(); | |
| this.wowEvent = createEvent(this.config.boxClass); | |
| } | |
| init() { | |
| this.element = window.document.documentElement; | |
| if (isIn(document.readyState, ['interactive', 'complete'])) { | |
| this.start(); | |
| } else { | |
| addEvent(document, 'DOMContentLoaded', this.start); | |
| } | |
| this.finished = []; | |
| } | |
| start() { | |
| this.stopped = false; | |
| this.boxes = [].slice.call(this.element.querySelectorAll(`.${this.config.boxClass}`)); | |
| this.all = this.boxes.slice(0); | |
| if (this.boxes.length) { | |
| if (this.disabled()) { | |
| this.resetStyle(); | |
| } else { | |
| for (let i = 0; i < this.boxes.length; i++) { | |
| const box = this.boxes[i]; | |
| this.applyStyle(box, true); | |
| } | |
| } | |
| } | |
| if (!this.disabled()) { | |
| addEvent(this.config.scrollContainer || window, 'scroll', this.scrollHandler); | |
| addEvent(window, 'resize', this.scrollHandler); | |
| this.interval = setInterval(this.scrollCallback, 50); | |
| } | |
| if (this.config.live) { | |
| const mut = new MutationObserver(records => { | |
| for (let j = 0; j < records.length; j++) { | |
| const record = records[j]; | |
| for (let k = 0; k < record.addedNodes.length; k++) { | |
| const node = record.addedNodes[k]; | |
| this.doSync(node); | |
| } | |
| } | |
| return undefined; | |
| }); | |
| mut.observe(document.body, { | |
| childList: true, | |
| subtree: true, | |
| }); | |
| } | |
| } | |
| // unbind the scroll event | |
| stop() { | |
| this.stopped = true; | |
| removeEvent(this.config.scrollContainer || window, 'scroll', this.scrollHandler); | |
| removeEvent(window, 'resize', this.scrollHandler); | |
| if (this.interval != null) { | |
| clearInterval(this.interval); | |
| } | |
| } | |
| sync() { | |
| if (MutationObserver.notSupported) { | |
| this.doSync(this.element); | |
| } | |
| } | |
| doSync(element) { | |
| if (typeof element === 'undefined' || element === null) { ({ element } = this); } | |
| if (element.nodeType !== 1) { return; } | |
| element = element.parentNode || element; | |
| const iterable = element.querySelectorAll(`.${this.config.boxClass}`); | |
| for (let i = 0; i < iterable.length; i++) { | |
| const box = iterable[i]; | |
| if (!isIn(box, this.all)) { | |
| this.boxes.push(box); | |
| this.all.push(box); | |
| if (this.stopped || this.disabled()) { | |
| this.resetStyle(); | |
| } else { | |
| this.applyStyle(box, true); | |
| } | |
| this.scrolled = true; | |
| } | |
| } | |
| } | |
| // show box element | |
| show(box) { | |
| this.applyStyle(box); | |
| box.className = `${box.className} ${this.config.animateClass}`; | |
| if (this.config.callback != null) { this.config.callback(box); } | |
| emitEvent(box, this.wowEvent); | |
| if (this.config.resetAnimation) { | |
| addEvent(box, 'animationend', this.resetAnimation); | |
| addEvent(box, 'oanimationend', this.resetAnimation); | |
| addEvent(box, 'webkitAnimationEnd', this.resetAnimation); | |
| addEvent(box, 'MSAnimationEnd', this.resetAnimation); | |
| } | |
| return box; | |
| } | |
| applyStyle(box, hidden) { | |
| const duration = box.getAttribute('data-wow-duration'); | |
| const delay = box.getAttribute('data-wow-delay'); | |
| const iteration = box.getAttribute('data-wow-iteration'); | |
| return this.animate(() => this.customStyle(box, hidden, duration, delay, iteration)); | |
| } | |
| animate = (function animateFactory() { | |
| if ('requestAnimationFrame' in window) { | |
| return callback => window.requestAnimationFrame(callback); | |
| } | |
| return callback => callback(); | |
| }()); | |
| resetStyle() { | |
| for (let i = 0; i < this.boxes.length; i++) { | |
| const box = this.boxes[i]; | |
| box.style.visibility = 'visible'; | |
| } | |
| return undefined; | |
| } | |
| resetAnimation(event) { | |
| if (event.type.toLowerCase().indexOf('animationend') >= 0) { | |
| const target = event.target || event.srcElement; | |
| target.className = target.className.replace(this.config.animateClass, '').trim(); | |
| } | |
| } | |
| customStyle(box, hidden, duration, delay, iteration) { | |
| if (hidden) { this.cacheAnimationName(box); } | |
| box.style.visibility = hidden ? 'hidden' : 'visible'; | |
| if (duration) { this.vendorSet(box.style, { animationDuration: duration }); } | |
| if (delay) { this.vendorSet(box.style, { animationDelay: delay }); } | |
| if (iteration) { this.vendorSet(box.style, { animationIterationCount: iteration }); } | |
| this.vendorSet(box.style, { animationName: hidden ? 'none' : this.cachedAnimationName(box) }); | |
| return box; | |
| } | |
| vendors = ['moz', 'webkit']; | |
| vendorSet(elem, properties) { | |
| for (const name in properties) { | |
| if (properties.hasOwnProperty(name)) { | |
| const value = properties[name]; | |
| elem[`${name}`] = value; | |
| for (let i = 0; i < this.vendors.length; i++) { | |
| const vendor = this.vendors[i]; | |
| elem[`${vendor}${name.charAt(0).toUpperCase()}${name.substr(1)}`] = value; | |
| } | |
| } | |
| } | |
| } | |
| vendorCSS(elem, property) { | |
| const style = getComputedStyle(elem); | |
| let result = style.getPropertyCSSValue(property); | |
| for (let i = 0; i < this.vendors.length; i++) { | |
| const vendor = this.vendors[i]; | |
| result = result || style.getPropertyCSSValue(`-${vendor}-${property}`); | |
| } | |
| return result; | |
| } | |
| animationName(box) { | |
| let aName; | |
| try { | |
| aName = this.vendorCSS(box, 'animation-name').cssText; | |
| } catch (error) { // Opera, fall back to plain property value | |
| aName = getComputedStyle(box).getPropertyValue('animation-name'); | |
| } | |
| if (aName === 'none') { | |
| return ''; // SVG/Firefox, unable to get animation name? | |
| } | |
| return aName; | |
| } | |
| cacheAnimationName(box) { | |
| // https://bugzilla.mozilla.org/show_bug.cgi?id=921834 | |
| // box.dataset is not supported for SVG elements in Firefox | |
| return this.animationNameCache.set(box, this.animationName(box)); | |
| } | |
| cachedAnimationName(box) { | |
| return this.animationNameCache.get(box); | |
| } | |
| // fast window.scroll callback | |
| scrollHandler() { | |
| this.scrolled = true; | |
| } | |
| scrollCallback() { | |
| if (this.scrolled) { | |
| this.scrolled = false; | |
| const results = []; | |
| for (let i = 0; i < this.boxes.length; i++) { | |
| const box = this.boxes[i]; | |
| if (box) { | |
| if (this.isVisible(box)) { | |
| this.show(box); | |
| continue; | |
| } | |
| results.push(box); | |
| } | |
| } | |
| this.boxes = results; | |
| if (!this.boxes.length && !this.config.live) { | |
| this.stop(); | |
| } | |
| } | |
| } | |
| // Calculate element offset top | |
| offsetTop(element) { | |
| // SVG elements don't have an offsetTop in Firefox. | |
| // This will use their nearest parent that has an offsetTop. | |
| // Also, using ('offsetTop' of element) causes an exception in Firefox. | |
| while (element.offsetTop === undefined) { | |
| element = element.parentNode; | |
| } | |
| let top = element.offsetTop; | |
| while (element.offsetParent) { | |
| element = element.offsetParent; | |
| top += element.offsetTop; | |
| } | |
| return top; | |
| } | |
| // check if box is visible | |
| isVisible(box) { | |
| const offset = box.getAttribute('data-wow-offset') || this.config.offset; | |
| const viewTop = ( | |
| this.config.scrollContainer && this.config.scrollContainer.scrollTop | |
| ) || window.pageYOffset; | |
| const viewBottom = | |
| viewTop + Math.min(this.element.clientHeight, getInnerHeight()) - offset; | |
| const top = this.offsetTop(box); | |
| const bottom = top + box.clientHeight; | |
| return top <= viewBottom && bottom >= viewTop; | |
| } | |
| disabled() { | |
| return !this.config.mobile && isMobile(navigator.userAgent); | |
| } | |
| } |