OwlCyberSecurity - MANAGER
Edit File: vanilla-marquee.js
// @ts-check /** * Shorthand for `document.getElementsByClassName` * * @param {String} selClass - The selector's class * @param {Element|HTMLElement|Document} [parent=document] - Parent element * * @returns {HTMLCollectionOf<Element>} - The selected elements */ function byClass(selClass, parent = document) { return parent.getElementsByClassName(selClass); } /** * Shorthand for `document.querySelector` * * @param {String} selector - Selector * @param {Element|HTMLElement|Document} [parent=document] - Parent element * * @returns {Element|HTMLElementTagNameMap|SVGElementTagNameMap|null} - The selected element */ function query(selector, parent = document) { return parent.querySelector(selector); } /** * Shorthand for `document.querySelectorAll` * * @param {String} selector - Selector * @param {Element|HTMLElement|Document} [parent=document] - Parent element * * @returns {NodeList} - The selected element */ function queryAll(selector, parent = document) { return parent.querySelectorAll(selector); } // @ts-check /** * Foreach polyfill for NodeList and HTMLCollection * https://toddmotto.com/ditch-the-array-foreach-call-nodelist-hack/ * * @param {Array<any>|NodeList|HTMLCollection} els - A list of elements * @param {foreachCB} fn - Callback containing ( value, index ) as arguments * @param {Function} [scope] - Scope */ function forEachHTML(els, fn, scope) { for (let i = 0, numEls = els.length; i < numEls; i++) fn.call(scope, els[i], i); } // @ts-check /** * Shorthand for `element.classList.add`, works with multiple nodes * * @param {Element|HTMLElement|HTMLCollection|NodeList} el - A list of elements * @param {...String} classes - Classes to add */ function addClass(el, ...classes) { // @ts-ignore if (el.length === undefined) { // @ts-ignore addClassEl(el, ...classes); } else { // @ts-ignore forEachHTML(el, currEl => { addClassEl(currEl, ...classes); }); } /** * Adds classes to a single element * * @param {Element|HTMLElement} elem - An HTML element * @param {...String} remClass - Classes to add */ function addClassEl(elem, ...remClass) { remClass.forEach(singleClass => { elem.classList.add(singleClass); }); } } // @ts-check /** * Shorthand for `element.addEventListener` * * @param {Element|HTMLElement|Window|Document|MediaQueryList} el - A list of elements * @param {String} ev - Event's name * @param {EventListenerOrEventListenerObject} fn - Event's function * @param {Object} [opts] - Optional event options */ function addEvent(el, ev, fn, opts) { el.addEventListener(ev, fn, opts); } /** * Shorthand for `element.removeEventListener` * * @param {Element|HTMLElement|Window|Document|MediaQueryList} el - A list of elements * @param {String} ev - Event's name * @param {EventListenerOrEventListenerObject} fn - Event's function * @param {Object} [opts] - Optional event options */ function removeEvent(el, ev, fn, opts) { el.removeEventListener(ev, fn, opts); } // @ts-check /** * Shorthand for `element.getAttribute` * * @param {Element|HTMLElement} el - An HTML element * @param {String} attr - The attribute to retrieve * * @returns {String|null} - The attribute's value */ function getAttr(el, attr) { return el.getAttribute(attr); } /** * Shorthand for `element.setAttribute` * * @param {Element|HTMLElement} el - An HTML element * @param {String} attr - The attribute to retrieve * @param {String} val - The value to set to the attribute */ function setAttr(el, attr, val) { el.setAttribute(attr, val); } /** * Shorthand for `element.removeAttribute` * * @param {Element|HTMLElement} el - An HTML element * @param {String} attr - The attribute to remove */ function remAttr(el, attr) { el.removeAttribute(attr); } /** * @typedef defaultOptions * @property {String} [css3easing='linear'] - A css3 transtion timing * @property {Number} [delayBeforeStart=1000] - Time in milliseconds before the marquee starts animating * @property {String} [direction='left'] - Direction towards which the marquee will animate ` 'left' | 'right' | 'up' | 'down'` * @property {Boolean} [duplicated=false] - Should the marquee be duplicated to show an effect of continuous flow. Use this only when the text is shorter than the container * @property {Number} [duration=5000] - Duration in milliseconds in which you want your element to travel * @property {Number} [gap=20] - Gap in pixels between the tickers. Will work only when the `duplicated` option is set to `true` * @property {Boolean} [pauseOnHover=false] - Pause the marquee on hover * @property {Boolean} [recalcResize=false] - Recalculate the marquee position on resize (breaks compatibility with jquery.marquee) * @property {Number} [speed=0] - Speed will override duration. Speed allows you to set a relatively constant marquee speed regardless of the width of the containing element. Speed is measured in pixels/second * @property {Boolean} [startVisible=false] - The marquee will be visible from the start if set to `true` */ const defOpts = { css3easing: 'linear', delayBeforeStart: 1000, direction: 'left', duplicated: false, duration: 5000, gap: 20, pauseOnHover: false, recalcResize: false, speed: 0, startVisible: false, }; let instances = 0; /** * Vanilla js marquee based on jQuery.marquee * https://github.com/aamirafridi/jQuery.Marquee */ class marquee { /** * Constructor * * @param {Element} el - The element where the marquee is applied * @param {defaultOptions} opts - the options */ constructor( el, opts ) { if ( typeof el === 'undefined' ) throw new Error( 'el cannot be undefined' ); if ( typeof el === 'string' ) throw new Error( 'el cannot be just a selector' ); if ( el === null ) throw new Error( 'el cannot be null' ); opts = { ...defOpts, ...opts, }; this.el = el; this._loopCount = 3; // Check for data-option since they have top priority for ( const option in defOpts ) { let currData = getAttr( el, `data-${defOpts[option]}` ); if ( currData !== null && currData !== '' ) { if ( currData === 'true' || currData === 'false' ) currData = Boolean( currData ); opts[option] = currData; } } // Reintroduce speed as an option. It calculates duration as a factor of the container width // measured in pixels per second. if ( opts.speed ) opts.duration = parseInt( el.clientWidth ) / opts.speed * 1000; // no gap if not duplicated opts.gap = opts.duplicated ? parseInt( opts.gap ) : 0; // wrap inner content into a div el.innerHTML = `<div class="js-marquee">${el.innerHTML}</div>`; // Make a copy of the element const marq = byClass( 'js-marquee', el )[0]; marq.style.marginRight = `${opts.gap}px`; marq.style.willChange = 'transform'; marq.style.float = 'left'; if ( opts.duplicated ) el.appendChild( marq.cloneNode( true ) ); // wrap both inner elements into one div el.innerHTML = `<div style="width:100000px" class="js-marquee-wrapper">${el.innerHTML}</div>`; // Save the reference of the wrapper const marqWrap = byClass( 'js-marquee-wrapper', el )[0], vertical = ( opts.direction === 'up' || opts.direction === 'down' ); this._marqWrap = marqWrap; this._vertical = vertical; this._duration = opts.duration; this._opts = opts; this._calcSizes(); const animationName = `marqueeAnimation-${Math.floor( Math.random() * 10000000 )}`, animStr = this._animationStr( animationName, opts.duration / 1000, opts.delayBeforeStart / 1000, 'infinite', ); this._animName = animationName; this._animStr = animStr; // if duplicated option is set to true than position the wrapper if ( opts.duplicated ) { if ( vertical ) { if ( opts.startVisible ) this._marqWrap.style.transform = 'translateY(0px)'; else this._marqWrap.style.transform = `translateY(${opts.direction === 'up' ? this._contHeight : ( -1 * ( ( this._elHeight * 2 ) - opts.gap ) )}px)`; } else { if ( opts.startVisible ) // eslint-disable-line no-lonely-if this._marqWrap.style.transform = 'translateX(0px)'; else this._marqWrap.style.transform = `translateX(${opts.direction === 'left' ? this._contWidth : ( -1 * ( ( this._elWidth * 2 ) - opts.gap ) )}px)`; } // If the text starts out visible we can skip the two initial loops if ( !opts.startVisible ) this._loopCount = 1; } else if ( opts.startVisible ) { // We only have two different loops if marquee is duplicated and starts visible this._loopCount = 2; } else { if ( vertical ) // eslint-disable-line no-lonely-if this._repositionVert(); else this._repositionHor(); } addEvent( this.el, 'pause', this.pause.bind( this ) ); addEvent( this.el, 'resume', this.resume.bind( this ) ); if ( opts.pauseOnHover ) { addEvent( this.el, 'mouseover', this.pause.bind( this ) ); addEvent( this.el, 'mouseout', this.resume.bind( this ) ); } /** * Method for animation end event */ this._animEnd = () => { this._animate( vertical ); this.el.dispatchEvent( new CustomEvent( 'finished' ) ); }; this._instance = instances; instances++; this._animate( vertical ); if ( opts.recalcResize ) addEvent( window, 'resize', this._recalcResize.bind( this ) ); } /** * Build the css string for the animation * * @privte * @param {String} [name=''] - animation name * @param {Number} [duration=0] - Animation duration (in s) * @param {Number} [delay=0] - Animation delay before starting (in s) * @param {String} [loops=''] - Animation iterations * * @returns {String} css animation string */ _animationStr( name = '', duration = 0, delay = 0, loops = '' ) { return `${name} ${duration}s ${delay}s ${loops} ${this._opts.css3easing}`; } /** * Animation of the marquee * * @private * @param {Boolean} vertical - Vertical direction */ _animate( vertical = false ) { const opts = this._opts; if ( opts.duplicated ) { // When duplicated, the first loop will be scroll longer so double the duration if ( this._loopCount === 1 ) { let duration = opts.duration; if ( vertical ) duration = ( opts.direction === 'up' ) ? duration + ( this._contHeight / ( this._elHeight / duration ) ) : duration * 2; else duration = ( opts.direction === 'left' ) ? duration + ( this._contWidth / ( this._elWidth / duration ) ) : duration * 2; this._animStr = this._animationStr( this._animName, duration / 1000, opts.delayBeforeStart / 1000, ); // On 2nd loop things back to normal, normal duration for the rest of animations } else if ( this._loopCount === 2 ) { this._animName = `${this._animName}0`; this._animStr = this._animationStr( this._animName, opts.duration / 1000, 0, 'infinite', ); } this._loopCount++; } let animationCss = ''; if ( vertical ) { if ( opts.duplicated ) { // Adjust the starting point of animation only when first loops finishes if ( this._loopCount > 2 ) this._marqWrap.style.transform = `translateY(${( opts.direction === 'up' ) ? 0 : -1 * this._elHeight}px)`; animationCss = `translateY(${( opts.direction === 'up' ) ? -1 * this._elHeight : 0}px)`; } else if ( opts.startVisible ) { // This loop moves the marquee out of the container if ( this._loopCount === 2 ) { // Adjust the css3 animation as well this._animStr = this._animationStr( this._animName, opts.duration / 1000, opts.delayBeforeStart / 1000, ); animationCss = `translateY(${( opts.direction === 'up' ) ? -1 * this._elHeight : this._contHeight}px)`; this._loopCount++; } else if ( this._loopCount === 3 ) { this._animName = `${this._animName}0`; this._animStr = this._animationStr( this._animName, this._completeDuration / 1000, 0, 'infinite', ); this._repositionVert(); } } else { this._repositionVert(); animationCss = `translateY(${( opts.direction === 'up' ) ? -1 * this._marqWrap.clientHeight : this._contHeight}px)`; } } else { if ( opts.duplicated ) { // eslint-disable-line no-lonely-if // Adjust the starting point of animation only when first loops finishes if ( this._loopCount > 2 ) this._marqWrap.style.transform = `translateX(${( opts.direction === 'left' ) ? 0 : -1 * this._elWidth}px)`; animationCss = `translateX(${( opts.direction === 'left' ) ? -1 * this._elWidth : 0}px)`; } else if ( opts.startVisible ) { // This loop moves the marquee out of the container if ( this._loopCount === 2 ) { // Adjust the css3 animation as well this._animStr = this._animationStr( this._animName, opts.duration / 1000, opts.delayBeforeStart / 1000, ); animationCss = `translateX(${( opts.direction === 'left' ) ? -1 * this._elWidth : this._contWidth}px)`; this._loopCount++; } else if ( this._loopCount === 3 ) { // Adjust the animation this._animName = `${this._animName}0`; this._animStr = this._animationStr( this._animName, opts.duration / 1000, 0, 'infinite', ); this._repositionHor(); } } else { this._repositionHor(); animationCss = `translateX(${( opts.direction === 'left' ) ? -1 * this._elWidth : this._contWidth}px)`; } } // fire event this.el.dispatchEvent( new CustomEvent( 'beforeStarting' ) ); // Append animation this._marqWrap.style.animation = this._animStr; const keyFrameCss = `@keyframes ${this._animName} { 100% { transform: ${animationCss}; } }`, styles = queryAll( 'style', this._marqWrap ); if ( styles.length ) styles[styles.length - 1].innerHTML = keyFrameCss; else if ( byClass( `marq-wrap-style-${this._instance}` ).length ) byClass( `marq-wrap-style-${this._instance}` )[0].innerHTML = keyFrameCss; else { const styleEl = document.createElement( 'style' ); addClass( styleEl, `marq-wrap-style-${this._instance}` ); styleEl.innerHTML = keyFrameCss; query( 'head' ).appendChild( styleEl ); } // Animation iteration event addEvent( this._marqWrap, 'animationiteration', this._animIter.bind( this ), { once: true, }); // Animation stopped addEvent( this._marqWrap, 'animationend', this._animEnd.bind( this ), { once: true, }); this._status = 'running'; setAttr( this.el, 'data-runningStatus', 'resumed' ); } /** * Event fired on Animation iteration * * @private */ _animIter() { this.el.dispatchEvent( new CustomEvent( 'finished' ) ); } /** * Reposition the Wrapper vertically * * @private */ _repositionVert() { this._marqWrap.style.transform = `translateY(${this._opts.direction === 'up' ? this._contHeight : ( this._elHeight * -1 )}px)`; } /** * Reposition the Wrapper horizontally * * @private */ _repositionHor() { this._marqWrap.style.transform = `translateX(${this._opts.direction === 'left' ? this._contWidth : ( this._elWidth * -1 )}px)`; } /** * Calculates the speed and the dimension of the marquee * * @private */ _calcSizes() { const el = this.el, opts = this._opts; // If direction is up or down, get the height of main element if ( this._vertical ) { const contHeight = el.clientHeight; this._contHeight = contHeight; remAttr( this._marqWrap, 'style' ); el.style.clientHeight = `${contHeight}px`; const marqs = byClass( 'js-marquee', el ), marqNums = marqs.length - 1; // Change the CSS for js-marquee element forEachHTML( marqs, ( currEl, ind ) => { currEl.style.float = 'none'; currEl.style.marginRight = '0px'; // Remove bottom margin from 2nd element if duplicated if ( opts.duplicated && ind === marqNums ) currEl.style.marginBottom = '0px'; else currEl.style.marginBottom = `${opts.gap}px`; }); const elHeight = parseInt( marqs[0].clientHeight + opts.gap ); this._elHeight = elHeight; // adjust the animation duration according to the text length if ( opts.startVisible && !opts.duplicated ) { // Compute the complete animation duration and save it for later reference // formula is to: (Height of the text node + height of the main container / Height of the main container) * duration; this._completeDuration = ( elHeight + contHeight ) / parseInt( contHeight ) * this._duration; // eslint-disable-line max-len opts.duration = elHeight / parseInt( contHeight ) * this._duration; } else // formula is to: (Height of the text node + height of the main container / Height of the main container) * duration; opts.duration = elHeight / parseInt( contHeight ) / parseInt( contHeight ) * this._duration; } else { // Save the width of the each element so we can use it in animation const elWidth = parseInt( byClass( 'js-marquee', el )[0].clientWidth + opts.gap ), contWidth = el.clientWidth; this._contWidth = contWidth; this._elWidth = elWidth; // adjust the animation duration according to the text length if ( opts.startVisible && !opts.duplicated ) { // Compute the complete animation duration and save it for later reference // formula is to: (Width of the text node + width of the main container / Width of the main container) * duration; this._completeDuration = ( elWidth + contWidth ) / parseInt( contWidth ) * this._duration; // (Width of the text node / width of the main container) * duration opts.duration = elWidth / parseInt( contWidth ) * this._duration; } else // formula is to: (Width of the text node + width of the main container / Width of the main container) * duration; opts.duration = ( elWidth + parseInt( contWidth ) ) / parseInt( contWidth ) * this._duration; // eslint-disable-line max-len } // if duplicated then reduce the duration if ( opts.duplicated ) opts.duration = opts.duration / 2; } /** * Recalculates the dimensions and positon of the marquee on page resize * * @private */ _recalcResize() { this._calcSizes(); this._loopCount = 2; this._animEnd(); } /** * Pause the animation */ pause() { this._marqWrap.style.animationPlayState = 'paused'; this._status = 'paused'; setAttr( this.el, 'data-runningStatus', 'paused' ); this.el.dispatchEvent( new CustomEvent( 'paused' ) ); } /** * Resume the animation */ resume() { this._marqWrap.style.animationPlayState = 'running'; this._status = 'running'; setAttr( this.el, 'data-runningStatus', 'resumed' ); this.el.dispatchEvent( new CustomEvent( 'resumed' ) ); } /** * Toggle animation playing status */ toggle() { if ( this._status === 'paused' ) this.resume(); else if ( this._status === 'running' ) this.pause(); } /** * Destorys the instance and removes events */ destroy() { removeEvent( this.el, 'pause', this.pause.bind( this ) ); removeEvent( this.el, 'resume', this.resume.bind( this ) ); if ( this._opts.pauseOnHover ) { removeEvent( this.el, 'mouseover', this.pause.bind( this ) ); removeEvent( this.el, 'mouseout', this.resume.bind( this ) ); } removeEvent( this._marqWrap, 'animationiteration', this._animIter.bind( this ), { once: true, }); removeEvent( this._marqWrap, 'animationend', this._animEnd.bind( this ), { once: true, }); if ( this._opts.recalcResize ) removeEvent( window, 'resize', this._recalcResize.bind( this ) ); } /** * Forces a refresh (like recalcResize) but done manually */ refresh() { this._recalcResize(); } }