PageSourceSearch

https://typefaceapp.com/_themes/typeface/js/main.js?v=1776263378

js typefaceapp.com collected 2026-10-02 05:11:57 UTC 18,899 bytes, 657 lines download raw bytes

1
2
3function onLoad() {
4
5    // mobile menu toggle
6    const $menuToggle = document.querySelector('.menu__toggle');
7    if ($menuToggle != null) {
8      $menuToggle.addEventListener('click', function(e) {
9        $menuToggle.parentNode.classList.toggle('menu__toggle--enabled');
10      });
11    }
12
13    const $reviews = document.querySelector('.reviews');
14    if ($reviews != null) {
15      $more = $reviews.querySelector('.read-more');
16      $more.addEventListener('click', function(e) {
17        e.preventDefault();
18        $reviews.classList.toggle('reviews--expanded');
19        $more.classList.toggle('read-more--expand');
20        $more.classList.toggle('read-more--collapse');
21      })
22    }
23
24
25    // fox speech bubbles
26    const $getAppElement = document.querySelector('.get-app');
27    if ($getAppElement != null) {
28
29      const foxStatements = [
30          {text: 'sup?', link: '/fox?vid=lick'},
31          {text: 'me all day', link: '/fox?vid=jump'},
32          {text: 'fraka-kaka', link: '/fox?vid=say'},
33          {text: 'wiggle', link: '/fox?vid=wiggle'},
34          {text: 'starring me', link: '/fox?vid=mr'},
35          {text: 'not me', link: '/fox?vid=stuck'}
36      ];
37
38      const $foxElement = $getAppElement.querySelector('.get-app__fox-statement');
39      const $iconElement = $getAppElement.querySelector('.get-app__icon');
40
41      let currentStatement = -1;
42
43      $iconElement.addEventListener('mouseenter', function() {
44
45          currentStatement = (currentStatement + 1) % foxStatements.length;
46          const statement = foxStatements[ currentStatement ];
47          $foxElement.setAttribute('href', statement.link);
48          $foxElement.textContent = statement.text;
49
50      });
51    }
52
53    const $starField = document.querySelector('#dark-mode-stars');
54    if ($starField != null) {
55      const sf = StarField($starField);
56    }
57
58    const $overviewScreen = document.querySelector('.focused__grid');
59    let overviewScreenTranslation = 0
60    if ($overviewScreen != null) {
61      scroller.watch($overviewScreen, pos => {
62        if (!pos.isVisible) {
63          return;
64        }
65
66        const translate = Math.max(0, pos.progress - 0.035) * 40 * -1;
67
68        // only update if necessary
69        if (overviewScreenTranslation === translate) {
70          return;
71        }
72
73        overviewScreenTranslation = translate
74
75        nextFrame(() => {
76          $overviewScreen.style.transform = `translateY(${translate}%)`;
77        })
78      })
79    }
80
81    const $overviewListScreen = document.querySelector('.focused__list');
82    let overviewListScreenTranslation = 0
83    if ($overviewListScreen != null) {
84      scroller.watch($overviewListScreen, pos => {
85        if (!pos.isVisible) {
86          return;
87        }
88        // start a bit later, not immediately at bottom
89        const startOffset = 0.035
90        // this is a percentage 0%...-38%, depends on image/container height ratio
91        const maxTranslate = 38
92        const translate = Math.max(0, pos.progress - startOffset) * maxTranslate * -1;
93
94        // only update if necessary
95        if (overviewListScreenTranslation === translate) {
96          return;
97        }
98
99        overviewListScreenTranslation = translate
100
101        nextFrame(() => {
102          $overviewListScreen.style.transform = `translateY(${translate}%)`;
103        })
104      })
105    }
106
107    const $duplicatesScreen = document.querySelector('.duplicates__image');
108    let duplicatesScreenTranslation = 0
109    if ($duplicatesScreen != null) {
110      scroller.watch($duplicatesScreen, pos => {
111        if (!pos.isVisible) {
112          return;
113        }
114
115        const translate = Math.round(pos.progress * -60 - 20);
116
117        // only update if necessary
118        if (duplicatesScreenTranslation === translate) {
119          return;
120        }
121
122        duplicatesScreenTranslation = translate
123
124        nextFrame(() => {
125          $duplicatesScreen.style.transform = `translateY(${translate}px)`;
126          $duplicatesScreen.querySelectorAll('.duplicates__check').forEach((el, idx) => {
127            el.style.display = pos.progress < idx * 0.1 + 0.2 ? "none" : "block"
128          })
129        })
130      })
131    }
132
133    const $pairingScreen = document.querySelector('.pairing__image');
134    let pairingScreenTranslation = 0
135    if ($pairingScreen != null) {
136      scroller.watch($pairingScreen, pos => {
137        if (!pos.isVisible) {
138          return;
139        }
140
141        const translate = Math.round(pos.progress * -50);
142
143        // only update if necessary
144        if (pairingScreenTranslation === translate) {
145          return;
146        }
147
148        pairingScreenTranslation = translate
149
150        nextFrame(() => {
151          $pairingScreen.style.transform = `translateY(${translate}px)`;
152          $pairingScreen.querySelectorAll('.pairing__candidate').forEach((el, idx) => {
153            el.style.display = pos.progress < idx * 0.14 + 0.25 ? "none" : "block"
154          })
155        })
156      })
157    }
158
159    const $headers = document.querySelectorAll('h1:not(.title)');
160    $headers.forEach($el => {
161
162      scroller.watch($el, pos => {
163        if (!pos.isVisible) {
164          return;
165        }
166
167        const start = Math.min(Math.max(0, pos.progress * 2.8 - 0.07), 1) * -120 + 20;
168        if (start <= -100) {
169          return; // perf
170        }
171        const center = start + 100;
172        const end = start + 200;
173
174        nextFrame(() => {
175          $el.style.backgroundImage = `linear-gradient(90deg, #7C10E1 ${start}%, #0093C8 ${center}%, #C95BD1 ${end}%)` // #0251C9
176        })
177
178      })
179
180    });
181
182    // Check if videos can be auto played, show controls if autoplay is disabled
183    const $videos = document.querySelectorAll('video[autoplay]');
184    $videos.forEach($el => {
185      // wait until enough data is available such that video can be played
186      // playing before this event may throw an error
187      // repro:
188      // 1. focus Safari (16.1) location input (no inspector)
189      // 2. type site (http://app.typeface.test:8888)
190      // 3. select item from dropdown suggestions
191      // => previously called .play() => would error
192      $el.addEventListener('canplaythrough', (event) => {
193        
194        // should typically autoplay by browser, wait a bit for that
195        setTimeout(() => {
196          
197          if ($el.paused) {
198            // video is not playing...
199            // try to force play
200            $el.play()
201              .catch(err => {
202                // playing failed, user has probably disabled
203                console.error("Could not play media", err);
204                // show controls to let user manualy play
205                $el.setAttribute("controls", "");
206              })
207          }
208          
209        }, 20)
210      });
211      
212    });
213    
214}
215
216
217function detectCheckout() {
218    if (window.fastspringIsReturning) { return; }
219
220    const session = getParameterByName('checkout') || getParameterByName('session')
221    if (session != null ) {
222      fastspring.builder.checkout(session);
223      return;
224    }
225
226    const purchase = getParameterByName('purchase')
227    if ( purchase != null) {
228
229        const coupon = getParameterByName('coupon');
230
231        setTimeout(function() {
232          checkout(parseInt(purchase, 10) || 1, coupon)
233        }, 500)
234    }
235}
236
237function checkout(quantity, coupon) {
238
239  const s = {
240    'coupon': coupon || "",
241    'reset': true, // reset the cart and session (will remove everything added to the cart or passed to the session prior to this)
242    'products' : [
243      {
244        'path': fastspringProductID,
245        'quantity': quantity
246      }
247    ],
248    'checkout': true, // once all of the above is done redirect to checkout
249  };
250
251  fastspring.builder.push(s);
252
253}
254
255function getParameterByName(name, url) {
256    if (!url) url = window.location.href;
257    name = name.replace(/[\[\]]/g, "\\$&");
258    var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
259        results = regex.exec(url);
260    if (!results) return null;
261    if (!results[2]) return '';
262    return decodeURIComponent(results[2].replace(/\+/g, " "));
263}
264
265function onFSPopupClosed(order) {
266    if (order) {
267        track('Order', 'Completed');
268    } else {
269        track('Order', 'Canceled');
270        document.body.classList.add('has-canceled');
271    }
272}
273
274function onFSError(code, reason) {
275  track('Error', 'FastSpring', 'FS ' + code + ' ' + reason)
276  document.body.classList.add('has-canceled');
277}
278
279function track(category, message, details) {
280  console.log(category, message, details);
281  // ga('send', 'event', category, message, details)
282}
283
284
285const ProductQuantity = {
286
287  template: `
288    <button class="purchase__users-decrease" @click="decrease">-</button>
289    <div class="purchase__users-count"><span class="purchase__users-quantity">{{ counter }}</span> {{ unit }}</div>
290    <button class="purchase__users-increase" @click="increase">+</button>
291  `,
292
293  data() {
294    return {
295      counter: 1,
296    }
297  },
298
299  computed: {
300    unit() {
301      return this.counter > 1 ? 'Users' : 'User'
302    },
303  },
304
305  watch: {
306    counter: {
307      handler(newValue) {
308        fastspring.builder.update(fastspringProductID, newValue)
309      },
310      // resets initial qty to 1 on launch
311      // Note that data-continuous must be true on FastSpring script tag, otherwise qty here might get out of sync with session qty
312      // e.g. open popup -> close popup -> session qty back to 0, but qty here still 1+
313      immediate: !window.fastspringIsReturning,
314    }
315  },
316
317  methods: {
318    decrease() {
319      this.setQuantity(this.counter - 1)
320    },
321    increase() {
322      this.setQuantity(this.counter + 1)
323    },
324    setQuantity(quantity) {
325      this.counter = Math.max(1, Math.min(quantity, 999))
326    },
327  }
328
329}
330
331
332const Search = {
333
334  template: `
335    <form class="search__form" @submit.prevent="search" v-if="isEnabled">
336    <div class="search__field">
337      <input type="search" class="search__input" placeholder="Search" v-model="searchString" @keydown.esc="reset" @input="search"/>
338      <button type="reset" class="search__reset" alt="Reset search" @click="reset"><img src="/assets/img/icon-close.svg" height="14" v-if="searchString.length"/></button>
339    </div>
340    <div class="search__results">
341      <span class="search__no-results" v-if="hasNoResults">No results found for "{{ searchedString }}"</span>
342      <a :href="r.url" class="search__result article" v-for="r in results">
343        <span class="article__title">{{ r.meta.title }}</span>
344        <span class="article__tags search__excerpt" v-html="r.excerpt"></span>
345      </a>
346      <a href="#" class="read-more" @click.prevent="loadResults" v-if="hasMoreResults">Load more results</a>
347      <hr v-if="results.length || hasNoResults"/>
348    </div>
349    </form>
350  `,
351
352  data() {
353    return {
354      searchString: "",
355      searchedString: null,
356      response: null,
357      results: [],
358      isEnabled: false,
359    }
360  },
361
362  computed: {
363    hasMoreResults() {
364      return this.response != null && this.response.results.length > this.results.length
365    },
366    hasNoResults() {
367      return this.response && this.response.results.length == 0
368    }
369  },
370
371  async mounted() {
372    try {
373      this.pagefind = await import("/assets/pagefind/pagefind.js");
374      await this.pagefind.options({
375          baseUrl: "/",
376          bundlePath: "/assets/pagefind/",
377      })
378      this.pagefind.init()
379      this.isEnabled = true
380    } catch {
381      // dummy
382      this.pagefind = {
383        async search() { return { results: [] } },
384        async debouncedSearch() { return { results: [] } },
385      }
386    }
387  },
388
389  methods: {
390    async search() {
391      const searchTerm = this.searchString
392      if (searchTerm.length == 0) {
393        return await this.reset()
394      }
395
396      const raw = await this.pagefind.debouncedSearch(searchTerm);
397      // may be null if debounced
398      if (raw != null) {
399        this.response = raw
400        this.results = []
401        this.searchedString = searchTerm
402        await this.loadResults()
403      }
404    },
405
406    async loadResults() {
407        const start = this.results.length
408        const end = start + 5
409        const results = await Promise.all(this.response.results.slice(start, end).map(r => r.data()))
410        for (const r of results) {
411            r.url = r.url.replace(/(\/|\.html)$/, "")
412            // #:~: text fragments only highlight first occurence
413        }
414        this.results = this.results.concat(results)
415    },
416
417    async reset() {
418      this.searchString = ""
419      this.searchedString = ""
420      this.response = null
421      this.results = []
422      // cancel previous search
423      await this.pagefind.debouncedSearch("", {}, 0);
424    }
425  }
426
427}
428
429
430detectCheckout()
431
432Vue.createApp(ProductQuantity).mount('#vue-product-quantity')
433Vue.createApp(Search).mount('#vue-search')
434
435if (document.readyState === "complete" || document.readyState === "interactive") {
436  onLoad();
437} else {
438  window.addEventListener('DOMContentLoaded', onLoad);
439}
440
441const nextFrame = window.requestAnimationFrame ||
442   window.webkitRequestAnimationFrame ||
443   window.mozRequestAnimationFrame ||
444   window.msRequestAnimationFrame ||
445   window.oRequestAnimationFrame ||
446   function(cb) { window.setTimeout(cb, 1000/60) }
447
448/**
449 * Scroller
450 * @constructor
451 */
452function Scroller() {
453
454  let subjects = []
455  let isStarted = false
456
457  let viewportTop = 0
458  let viewportHeight = 0
459  let viewportBottom = 0
460
461  // calculate y offset of element to top of document
462  const getYOffset = function (el) {
463    let y = 0
464    while( el && !isNaN(el.offsetTop) ) {
465      y += el.offsetTop
466      el = el.offsetParent
467    }
468    return y
469  }
470
471  this.watch = function(el, callback) {
472
473    if (el == null) {
474        return
475    }
476
477    const subject = { el: el, callback: callback }
478    subjects.push(subject)
479
480    if (!isStarted) {
481        window.addEventListener('scroll', () => this.didScroll())
482        this.updateViewport()
483        isStarted = true
484    }
485
486    // fire at least once
487    this.update(subject)
488  }
489
490  /// Remove all watchers for a particular element
491  this.removeWatchers = function(el) {
492    subjects = subjects.filter(s => s.el != el)
493  }
494
495  this.didScroll = function() {
496    // scrolling is synced with v-sync, so no need to requestAnimationFrame
497    // let callback decide if it should use requestAnimationFrame
498    // https://stackoverflow.com/questions/41740082/scroll-events-requestanimationframe-vs-requestidlecallback-vs-passive-event-lis
499    this.updateViewport()
500    subjects.forEach(subject => this.update(subject))
501  }
502
503  this.updateViewport = function() {
504    viewportTop = window.pageYOffset
505    viewportHeight = window.innerHeight
506    viewportBottom = viewportTop + viewportHeight
507  }
508
509  this.update = function(subject) {
510
511      newPos = this.calcPosition(subject.el)
512      if (!subject.pos || newPos.progress != subject.pos.progress) {
513        subject.callback(newPos)
514      }
515      subject.pos = newPos
516
517  }
518  // calculate the position on screen of the given dom element
519  this.calcPosition = function (el) {
520
521    const elTop = getYOffset(el),
522      elHeight = el.offsetHeight,
523      elBottom = elTop + elHeight;
524
525    // 0 if element top is on viewport bottom
526    // 1 if element bottom is on viewport top
527    const positioning = ( viewportBottom - elTop ) / ( viewportHeight + elHeight )
528
529    const fromTop = elTop - viewportTop
530    const fromBottom = viewportBottom - elBottom
531
532    return { progress: Math.max(0, Math.min( positioning, 1) ), fromTop: fromTop, fromBottom: fromBottom, isVisible: positioning >= 0 && positioning <= 1 }
533  }
534}
535
536const scroller = new Scroller()
537
538
539/**
540 * StarField
541 * @param       {[type]} canvas [description]
542 * @constructor
543 */
544function StarField(canvas) {
545
546  // https://codepen.io/iblamefish/pen/xgefG
547  const STAR_COLORS = ["#ffffff", "#ffe9c4", "#d4fbff"];
548  const STAR_COUNT = 1000;
549
550  // generate a random integer between min..<max
551  function random (min, max) {
552    // Math.random() = 0..<1
553    return Math.floor((Math.random() * (max - min)) + min);
554  }
555
556  function Star(fieldWidth, fieldHeight, render) {
557    // distribute, also outside of field to allow translating
558    this.x = Math.random() * fieldWidth * 1.1 - 0.05 * fieldWidth;
559    this.y = Math.random() * fieldHeight * 1.1;
560    // more small stars than big, with minimum size
561    this.radius = Math.random() * Math.random() * 12 + 2;
562    this.brightness = random(75, 100) / 100;
563    this.render = render;
564  }
565
566  function createStars(w, h) {
567
568    // offscreen render a few stars
569    // which are used to draw on the main canvas
570    // this improves performance
571    // https://codepen.io/giana/pen/qbWNYy
572    let prerenders = [];
573    for (let i = 0; i < STAR_COLORS.length; i++) {
574      const pcanvas = document.createElement('canvas');
575      const pctx = pcanvas.getContext('2d');
576      pcanvas.width = pcanvas.height = 30;
577      const center = pcanvas.width/2;
578
579      const color = STAR_COLORS[i % STAR_COLORS.length];
580      const gradient = pctx.createRadialGradient(center, center, 0, center, center, center);
581      // inner
582      gradient.addColorStop(0.025, '#fff');
583      gradient.addColorStop(0.1, color + 'bb');
584      // halo
585      gradient.addColorStop(0.2, color + '20');
586      gradient.addColorStop(1, 'transparent');
587
588      pctx.globalAlpha = 1;
589      pctx.fillStyle = gradient;
590
591      pctx.beginPath();
592      pctx.arc(center, center, center, 0, Math.PI * 2);
593      pctx.fill();
594
595      prerenders[i] = pcanvas;
596    }
597
598    // generate stars
599    let stars = [];
600    for (let i = 0; i < STAR_COUNT; i++) {
601      stars[i] = new Star(w, h, prerenders[i % prerenders.length]);
602    }
603
604    return stars
605
606  }
607
608  // render the star field
609  this.render = function() {
610
611    if (!this.pos.isVisible) {
612      // not in view, skip drawing
613      // and stop animation (wait until in view again)
614      return;
615    }
616
617    // reset transformations
618    ctx.setTransform(1, 0, 0, 1, 0, 0);
619    ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
620    ctx.globalCompositeOperation = 'lighter';
621
622    // rotate around point
623    ctx.translate(300, 300);
624    ctx.rotate(- this.pos.progress / 5);
625    ctx.translate(-300, -300);
626
627    for (const star of this.stars) {
628
629      const twinkle = Math.max(0, random(0, 600) - 500) / 200; // 1 in 7 times decrease by 0...0.5
630      const alpha = star.brightness - twinkle;
631      ctx.globalAlpha = alpha;
632
633      ctx.drawImage(star.render, star.x - star.radius, star.y - star.radius, star.radius * 2, star.radius * 2);
634    }
635
636    nextFrame(() => this.render());
637  }
638
639  // initialize
640  this.pos = null;
641  this.ctx = canvas.getContext('2d');
642  // make canvas size same as css size
643  canvas.width = canvas.offsetWidth;
644  canvas.height = canvas.offsetHeight;
645
646  this.stars = createStars(canvas.width, canvas.height);
647
648  // wait until into view
649  scroller.watch(canvas, pos => {
650    const wasVisible = this.pos && this.pos.isVisible;
651    this.pos = pos;
652    if (pos.isVisible && !wasVisible) {
653      // canvas visible, start rendering
654      nextFrame(() => this.render());
655    }
656  })
657}

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.