aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/utils.mjs
blob: dba023ac5bc7f71d06529b0e688f3fb7409227d7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
/**
 * Do callback when HTMLElement in removed
 *
 * @param {HTMLElement} element observing
 * @param {Function} callback
 */
export const onRemove = (element, callback) => {
  const parent = element.parentNode
  if (!parent) throw new Error('The node must already be attached')

  const obs = new window.MutationObserver(mutations => {
    for (const mutation of mutations) {
      for (const el of mutation.removedNodes) {
        if (el === element) {
          obs.disconnect()
          callback()
        }
      }
    }
  })
  obs.observe(parent, { childList: true })
}

/**
 * Animate transition of DOMRect, with Web Animation API
 *
 * @param {HTMLElement} element                 Element which animation applies
 * @param {DOMRect}     rect                    DOMRect for transition
 * @param {Object}      options
 * @param {Boolean}     options.resume          If true, transition starts from rect to DOMRect of element
 * @param {Number}      options.duration        Duration of animation in milliseconds
 * @returns {Animation} https://developer.mozilla.org/en-US/docs/Web/API/Animation
 */
export const animateRectTransition = (element, rect, options = {}) => {
  if (!element.parentElement) { throw new Error('The node must already be attached') }

  const { width: w1, height: h1, left: x1, top: y1 } = rect
  const {
    width: w2,
    height: h2,
    left: x2,
    top: y2
  } = element.getBoundingClientRect()

  const rw = (w1 ?? w2) / w2
  const rh = (h1 ?? h2) / h2
  const dx = x1 - x2
  const dy = y1 - y2

  if ((dx === 0 && dy === 0) || !isFinite(rw) || !isFinite(rh)) {
    return element.animate([], { duration: 0 })
  }

  const transform1 = 'translate(0, 0) scale(1, 1)'
  const transform2 = `translate(${dx}px, ${dy}px) scale(${rw}, ${rh})`
  const keyframes = [
    { transform: transform1, opacity: 1 },
    { transform: transform2, opacity: 0.3 }
  ]
  if (options.resume === true) keyframes.reverse()

  return element.animate(keyframes, {
    duration: options.duration ?? 500,
    easing: 'ease-in-out'
  })
}

/**
 * Throttle for function call
 *
 * @param {Function} func
 * @param {Number} delay milliseconds
 * @returns {Any} return value of function call, or null if throttled
 */
export function throttle (func, delay) {
  let timerFlag = null

  return function (...args) {
    const context = this
    if (timerFlag !== null) return null

    timerFlag = setTimeout(
      () => (timerFlag = null),
      typeof delay === 'function' ? delay.call(context) : delay
    )

    return func.call(context, ...args)
  }
}

/**
 * shiftByWindow. make sure HTMLElement inside viewport
 *
 * @param {HTMLElement} element
 */
export const shiftByWindow = element => {
  const rect = element.getBoundingClientRect()
  const offsetX = window.innerWidth - rect.left - rect.width
  const offsetY = window.innerHeight - rect.top - rect.height
  element.style.transform = `translate(${offsetX < 0 ? offsetX : 0}px, ${offsetY < 0 ? offsetY : 0}px)`
}

/**
 * insideWindow. check DOMRect is inside window
 *
 * @param {HTMLElement} element
 */
export const insideWindow = element => {
  const rect = element.getBoundingClientRect()
  return (
    rect.left > 0 &&
    rect.right < window.innerWidth + rect.width &&
    rect.top > 0 &&
    rect.bottom < window.innerHeight + rect.height
  )
}

/**
 * insideParent. check children element is inside DOMRect of parent element
 *
 * @param {HTMLElement} childElement
 * @param {HTMLElement} parentElement
 */
export const insideParent = (childElement, parentElement) => {
  const childRect = childElement.getBoundingClientRect()
  const parentRect = parentElement.getBoundingClientRect()
  const offset = 10

  return (
    childRect.left < parentRect.right - offset &&
    childRect.right > parentRect.left + offset &&
    childRect.top < parentRect.bottom - offset &&
    childRect.bottom > parentRect.top + offset
  )
}