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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
|
/**
* Do callback when HTMLElement in removed
*
* @param {HTMLElement} element observing
* @param {Function} callback
*/
export const onRemove = (element, callback) => {
const parent = element.parentElement
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)
}
}
/**
* debounce.
*
* @param {Function} func
* @param {Number} delay - milliseconds
*/
export function debounce (func, delay = 1000) {
let timer = null
return function (...args) {
const context = this
clearTimeout(timer)
timer = setTimeout(() => {
func.apply(context, args)
}, delay)
}
}
/**
* shiftByWindow. make sure HTMLElement inside viewport
*
* @param {HTMLElement} element
*/
export const shiftByWindow = element => {
delete element.style.transform
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
)
}
/**
* replaceTextNodes.
* @description Search current nodes by pattern, and replace them by new node
* @todo refactor to smaller methods
* @param {HTMLElement} rootNode
* @param {RegExp} pattern
* @param {Function} newNode - Create new node by each result of String.prototype.matchAll
*/
export const replaceTextNodes = (
rootNode,
pattern,
addNewNode = (match) => {
const link = document.createElement('a')
link.textContent(match.at(0))
return link
},
) => {
const nodeIterator = document.createNodeIterator(
rootNode,
window.NodeFilter.SHOW_TEXT,
node => node.textContent.match(pattern) && node.parentElement && !node.parentElement.closest('pre,code,a')
? window.NodeFilter.FILTER_ACCEPT
: window.NodeFilter.FILTER_REJECT,
)
let node = nodeIterator.nextNode()
const nodeArray = []
while (node) {
let index = 0
for (const match of node.textContent.matchAll(pattern)) {
const text = node.textContent.slice(index, match.index)
const newNode = addNewNode(match)
if (!newNode) continue
index = match.index + match.at(0).length
node.parentElement.insertBefore(document.createTextNode(text), node)
node.parentElement.insertBefore(newNode, node)
nodeArray.push(newNode)
}
if (index < node.textContent.length) {
const text = node.textContent.slice(index)
node.parentElement.insertBefore(document.createTextNode(text), node)
}
node.parentElement.removeChild(node)
node = nodeIterator.nextNode()
}
return nodeArray
}
/**
* Get the common ancestor of two or more elements
* {@link https://gist.github.com/kieranbarker/cd86310d0782b7c52ce90cd7f45bb3eb}
* @param {String} selector A valid CSS selector
* @returns {Element} The common ancestor
*/
export function getCommonAncestor (selector) {
// Get the elements matching the selector
const elems = document.querySelectorAll(selector)
// If there are no elements, return null
if (elems.length < 1) return null
// If there's only one element, return it
if (elems.length < 2) return elems[0]
// Otherwise, create a new Range
const range = document.createRange()
// Start at the beginning of the first element
range.setStart(elems[0], 0)
// Stop at the end of the last element
range.setEnd(
elems[elems.length - 1],
elems[elems.length - 1].childNodes.length,
)
// Return the common ancestor
return range.commonAncestorContainer
}
/**
* full2Half: full-Width Digits To Half-Width.
*
* @param {String} str
*/
export const full2Half = (str) => {
// Create a regular expression to match full-width digits (U+FF10 to U+FF19)
const fullWidthDigitsRegex = /[\uFF0E\uFF10-\uFF19]/g
// Replace full-width digits with their half-width equivalents
return str.replace(fullWidthDigitsRegex, (match) => {
const fullWidthDigit = match.charCodeAt(0)
const halfWidthDigit = fullWidthDigit - 65248 // Offset to convert full-width to half-width
return String.fromCharCode(halfWidthDigit)
})
}
|