PageSourceSearch

https://sarv.com/inc//js/home-card-effect.js

js sarv.com collected 2026-09-24 19:34:30 UTC 12,653 bytes, 412 lines download raw bytes

1// Utility function
2function Util () {};
3
4/* 
5	class manipulation functions
6*/
7Util.hasClass = function(el, className) {
8	if (el.classList) return el.classList.contains(className);
9	else return !!el.className.match(new RegExp('(\\s|^)' + className + '(\\s|$)'));
10};
11
12Util.addClass = function(el, className) {
13	var classList = className.split(' ');
14 	if (el.classList) el.classList.add(classList[0]);
15 	else if (!Util.hasClass(el, classList[0])) el.className += " " + classList[0];
16 	if (classList.length > 1) Util.addClass(el, classList.slice(1).join(' '));
17};
18
19Util.removeClass = function(el, className) {
20	var classList = className.split(' ');
21	if (el.classList) el.classList.remove(classList[0]);	
22	else if(Util.hasClass(el, classList[0])) {
23		var reg = new RegExp('(\\s|^)' + classList[0] + '(\\s|$)');
24		el.className=el.className.replace(reg, ' ');
25	}
26	if (classList.length > 1) Util.removeClass(el, classList.slice(1).join(' '));
27};
28// Sticky header reference
29var stickyHeader = document.querySelector('.sticky-header'); // Adjust selector
30var stickyHeaderHeight = stickyHeader ? stickyHeader.offsetHeight : 0;
31
32Util.toggleClass = function(el, className, bool) {
33	if(bool) Util.addClass(el, className);
34	else Util.removeClass(el, className);
35};
36
37Util.setAttributes = function(el, attrs) {
38  for(var key in attrs) {
39    el.setAttribute(key, attrs[key]);
40  }
41};
42
43/* 
44  DOM manipulation
45*/
46Util.getChildrenByClassName = function(el, className) {
47  var children = el.children,
48    childrenByClass = [];
49  for (var i = 0; i < el.children.length; i++) {
50    if (Util.hasClass(el.children[i], className)) childrenByClass.push(el.children[i]);
51  }
52  return childrenByClass;
53};
54
55Util.is = function(elem, selector) {
56  if(selector.nodeType){
57    return elem === selector;
58  }
59
60  var qa = (typeof(selector) === 'string' ? document.querySelectorAll(selector) : selector),
61    length = qa.length,
62    returnArr = [];
63
64  while(length--){
65    if(qa[length] === elem){
66      return true;
67    }
68  }
69
70  return false;
71};
72
73/* 
74	Animate height of an element
75*/
76Util.setHeight = function(start, to, element, duration, cb) {
77	var change = to - start,
78	    currentTime = null;
79
80  var animateHeight = function(timestamp){  
81    if (!currentTime) currentTime = timestamp;         
82    var progress = timestamp - currentTime;
83    var val = parseInt((progress/duration)*change + start);
84    element.style.height = val+"px";
85    if(progress < duration) {
86        window.requestAnimationFrame(animateHeight);
87    } else {
88    	cb();
89    }
90  };
91  
92  //set the height of the element before starting animation -> fix bug on Safari
93  element.style.height = start+"px";
94  window.requestAnimationFrame(animateHeight);
95};
96
97/* 
98	Smooth Scroll
99*/
100
101Util.scrollTo = function(final, duration, cb, scrollEl) {
102  var element = scrollEl || window;
103  var start = element.scrollTop || document.documentElement.scrollTop,
104    currentTime = null;
105
106  if(!scrollEl) start = window.scrollY || document.documentElement.scrollTop;
107      
108  var animateScroll = function(timestamp){
109  	if (!currentTime) currentTime = timestamp;        
110    var progress = timestamp - currentTime;
111    if(progress > duration) progress = duration;
112    var val = Math.easeInOutQuad(progress, start, final-start, duration);
113    element.scrollTo(0, val);
114    if(progress < duration) {
115        window.requestAnimationFrame(animateScroll);
116    } else {
117      cb && cb();
118    }
119  };
120
121  window.requestAnimationFrame(animateScroll);
122};
123
124/* 
125  Focus utility classes
126*/
127
128//Move focus to an element
129Util.moveFocus = function (element) {
130  if( !element ) element = document.getElementsByTagName("body")[0];
131  element.focus();
132  if (document.activeElement !== element) {
133    element.setAttribute('tabindex','-1');
134    element.focus();
135  }
136};
137
138/* 
139  Misc
140*/
141
142Util.getIndexInArray = function(array, el) {
143  return Array.prototype.indexOf.call(array, el);
144};
145
146Util.cssSupports = function(property, value) {
147  if('CSS' in window) {
148    return CSS.supports(property, value);
149  } else {
150    var jsProperty = property.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase();});
151    return jsProperty in document.body.style;
152  }
153};
154
155// merge a set of user options into plugin defaults
156// https://gomakethings.com/vanilla-javascript-version-of-jquery-extend/
157Util.extend = function() {
158  // Variables
159  var extended = {};
160  var deep = false;
161  var i = 0;
162  var length = arguments.length;
163
164  // Check if a deep merge
165  if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) {
166    deep = arguments[0];
167    i++;
168  }
169
170  // Merge the object into the extended object
171  var merge = function (obj) {
172    for ( var prop in obj ) {
173      if ( Object.prototype.hasOwnProperty.call( obj, prop ) ) {
174        // If deep merge and property is an object, merge properties
175        if ( deep && Object.prototype.toString.call(obj[prop]) === '[object Object]' ) {
176          extended[prop] = extend( true, extended[prop], obj[prop] );
177        } else {
178          extended[prop] = obj[prop];
179        }
180      }
181    }
182  };
183
184  // Loop through each object and conduct a merge
185  for ( ; i < length; i++ ) {
186    var obj = arguments[i];
187    merge(obj);
188  }
189
190  return extended;
191};
192
193// Check if Reduced Motion is enabled
194Util.osHasReducedMotion = function() {
195  if(!window.matchMedia) return false;
196  var matchMediaObj = window.matchMedia('(prefers-reduced-motion: reduce)');
197  if(matchMediaObj) return matchMediaObj.matches;
198  return false; // return false if not supported
199}; 
200
201/* 
202	Polyfills
203*/
204//Closest() method
205if (!Element.prototype.matches) {
206	Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
207}
208
209if (!Element.prototype.closest) {
210	Element.prototype.closest = function(s) {
211		var el = this;
212		if (!document.documentElement.contains(el)) return null;
213		do {
214			if (el.matches(s)) return el;
215			el = el.parentElement || el.parentNode;
216		} while (el !== null && el.nodeType === 1); 
217		return null;
218	};
219}
220
221//Custom Event() constructor
222if ( typeof window.CustomEvent !== "function" ) {
223
224  function CustomEvent ( event, params ) {
225    params = params || { bubbles: false, cancelable: false, detail: undefined };
226    var evt = document.createEvent( 'CustomEvent' );
227    evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
228    return evt;
229   }
230
231  CustomEvent.prototype = window.Event.prototype;
232
233  window.CustomEvent = CustomEvent;
234}
235
236/* 
237	Animation curves
238*/
239Math.easeInOutQuad = function (t, b, c, d) {
240	t /= d/2;
241	if (t < 1) return c/2*t*t + b;
242	t--;
243	return -c/2 * (t*(t-2) - 1) + b;
244};
245
246
247/* JS Utility Classes */
248(function() {
249  // make focus ring visible only for keyboard navigation (i.e., tab key) 
250  var focusTab = document.getElementsByClassName('js-tab-focus');
251  function detectClick() {
252    if(focusTab.length > 0) {
253      resetFocusTabs(false);
254      window.addEventListener('keydown', detectTab);
255    }
256    window.removeEventListener('mousedown', detectClick);
257  };
258
259  function detectTab(event) {
260    if(event.keyCode !== 9) return;
261    resetFocusTabs(true);
262    window.removeEventListener('keydown', detectTab);
263    window.addEventListener('mousedown', detectClick);
264  };
265
266  function resetFocusTabs(bool) {
267    var outlineStyle = bool ? '' : 'none';
268    for(var i = 0; i < focusTab.length; i++) {
269      focusTab[i].style.setProperty('outline', outlineStyle);
270    }
271  };
272  window.addEventListener('mousedown', detectClick);
273}());
274// Tutorial - https://codyhouse.co/tutorials/how-stacking-cards
275(function() {
276  var StackCards = function(element) {
277    this.element = element;
278    this.items = this.element.getElementsByClassName('js-stack-cards__item');
279    this.scrollingFn = false;
280    this.scrolling = false;
281    initStackCardsEffect(this); 
282    initStackCardsResize(this); 
283  };
284
285  function initStackCardsEffect(element) { // use Intersection Observer to trigger animation
286    setStackCards(element); // store cards CSS properties
287		var observer = new IntersectionObserver(stackCardsCallback.bind(element), { threshold: [0, 1] });
288		observer.observe(element.element);
289  };
290
291  function initStackCardsResize(element) { // detect resize to reset gallery
292    element.element.addEventListener('resize-stack-cards', function(){
293      setStackCards(element);
294      animateStackCards.bind(element);
295    });
296  };
297  
298  function stackCardsCallback(entries) { // Intersection Observer callback
299    if(entries[0].isIntersecting) {
300      if(this.scrollingFn) return; // listener for scroll event already added
301      stackCardsInitEvent(this);
302    } else {
303      if(!this.scrollingFn) return; // listener for scroll event already removed
304      window.removeEventListener('scroll', this.scrollingFn);
305      this.scrollingFn = false;
306    }
307  };
308  
309  function stackCardsInitEvent(element) {
310    element.scrollingFn = stackCardsScrolling.bind(element);
311    window.addEventListener('scroll', element.scrollingFn);
312  };
313
314  function stackCardsScrolling() {
315    if(this.scrolling) return;
316    this.scrolling = true;
317    window.requestAnimationFrame(animateStackCards.bind(this));
318  };
319
320  function setStackCards(element) {
321    // store wrapper properties
322    element.marginY = getComputedStyle(element.element).getPropertyValue('--stack-cards-gap');
323    getIntegerFromProperty(element); // convert element.marginY to integer (px value)
324    element.elementHeight = element.element.offsetHeight;
325
326    // store card properties
327    var cardStyle = getComputedStyle(element.items[0]);
328  element.cardTop = Math.floor(parseFloat(cardStyle.getPropertyValue('top'))) - stickyHeaderHeight;
329        element.cardHeight = Math.floor(parseFloat(cardStyle.getPropertyValue('height')));
330        element.windowHeight = window.innerHeight;
331    // store window property
332    element.windowHeight = window.innerHeight;
333
334    // reset margin + translate values
335    if(isNaN(element.marginY)) {
336      element.element.style.paddingBottom = '50px';
337    } else {
338      element.element.style.paddingBottom = (element.marginY*(element.items.length - 1))+'px';
339    }
340
341    for(var i = 0; i < element.items.length; i++) {
342      if(isNaN(element.marginY)) {
343        element.items[i].style.transform = 'none;';
344      } else {
345        element.items[i].style.transform = 'translateY('+element.marginY*i+'px)';
346      }
347    }
348  };
349
350  function getIntegerFromProperty(element) {
351    var node = document.createElement('div');
352    node.setAttribute('style', 'opacity:0; visbility: hidden;position: absolute; height:'+element.marginY);
353    element.element.appendChild(node);
354    element.marginY = parseInt(getComputedStyle(node).getPropertyValue('height'));
355    element.element.removeChild(node);
356  };
357
358  function animateStackCards() {
359    if(isNaN(this.marginY)) { // --stack-cards-gap not defined - do not trigger the effect
360      this.scrolling = false;
361      return; 
362    }
363
364    var top = this.element.getBoundingClientRect().top;
365
366    if(this.cardTop - top + this.element.windowHeight - this.elementHeight - this.cardHeight + this.marginY + this.marginY*this.items.length + stickyHeaderHeight > 0) { 
367          this.scrolling = false;
368          return;
369        }
370
371    for(var i = 0; i < this.items.length; i++) { // use only scale
372      var scrolling = this.cardTop - top - i*(this.cardHeight+this.marginY);
373      if(scrolling > 0) {  
374        var scaling = i == this.items.length - 1 ? 1 : (this.cardHeight - scrolling*0.05)/this.cardHeight;
375        this.items[i].style.transform = 'translateY('+this.marginY*i+'px) scale('+scaling+')';
376      } else {
377        this.items[i].style.transform = 'translateY('+this.marginY*i+'px)';
378      }
379    }
380
381    this.scrolling = false;
382  };
383
384  // initialize StackCards object
385  var stackCards = document.getElementsByClassName('js-stack-cards'),
386    intersectionObserverSupported = ('IntersectionObserver' in window && 'IntersectionObserverEntry' in window && 'intersectionRatio' in window.IntersectionObserverEntry.prototype),
387    reducedMotion = Util.osHasReducedMotion();
388    
389	if(stackCards.length > 0 && intersectionObserverSupported && !reducedMotion) { 
390    var stackCardsArray = [];
391		for(var i = 0; i < stackCards.length; i++) {
392			(function(i){
393        stackCardsArray.push(new StackCards(stackCards[i]));
394      })(i);
395    }
396    
397    var resizingId = false,
398      customEvent = new CustomEvent('resize-stack-cards');
399    
400    window.addEventListener('resize', function() {
401      clearTimeout(resizingId);
402      resizingId = setTimeout(doneResizing, 500);
403    });
404
405    function doneResizing() {
406      for( var i = 0; i < stackCardsArray.length; i++) {
407        (function(i){stackCardsArray[i].element.dispatchEvent(customEvent)})(i);
408      };
409    };
410	}
411}());
412

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.