@splendidlabz/utils 1.8.1 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/cjs/dom/index.cjs +90 -0
- package/dist/cjs/dom/observers/index.cjs +92 -2
- package/dist/cjs/dom/observers/scroll-observer.cjs +170 -0
- package/dist/cjs/lib/functions/functional.cjs +20 -0
- package/dist/cjs/lib/functions/index.cjs +20 -0
- package/dist/cjs/lib/index.cjs +20 -0
- package/dist/esm/dom/index.js +89 -0
- package/dist/esm/dom/observers/index.js +90 -1
- package/dist/esm/dom/observers/scroll-observer.js +144 -0
- package/dist/esm/lib/functions/functional.js +18 -0
- package/dist/esm/lib/functions/index.js +18 -0
- package/dist/esm/lib/index.js +18 -0
- package/dist/types/dom/index.d.cts +1 -0
- package/dist/types/dom/observers/index.d.cts +1 -0
- package/dist/types/dom/observers/scroll-observer.d.cts +27 -0
- package/dist/types/lib/functions/functional.d.cts +71 -5
- package/dist/types/lib/functions/index.d.cts +1 -1
- package/dist/types/lib/index.d.cts +1 -1
- package/package.json +1 -1
- package/src/dom/observers/index.js +1 -0
- package/src/dom/observers/resize-observer.js +5 -5
- package/src/dom/observers/scroll-observer.js +132 -0
- package/src/lib/functions/functional.js +82 -0
- package/src/lib/functions/functional.test.js +196 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/* eslint-env browser */
|
|
2
|
+
import { resizeObserver } from './resize-observer.js'
|
|
3
|
+
|
|
4
|
+
const defaultOptions = {
|
|
5
|
+
threshold: 0, // Float between 0 to 1.
|
|
6
|
+
tolerance: 0.1, // Float between 0 to 1. Tolerance for event firing
|
|
7
|
+
throttle: 16, // Throttle interval in ms (default: ~60fps)
|
|
8
|
+
once: false, // Only fire threshold callback once
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// TODO: Move to Utils/Dom
|
|
12
|
+
/**
|
|
13
|
+
* Scroll Observer - Optimized for performance
|
|
14
|
+
* @param {Element} node - The element to observe scroll events on
|
|
15
|
+
* @param {Object} options - Configuration options
|
|
16
|
+
* @param {Number} options.threshold - Float between 0 to 1. When to trigger callback (0.75 = 75% down page)
|
|
17
|
+
* @param {Number} options.tolerance - Float between 0 to 1. Tolerance zone around threshold
|
|
18
|
+
* @param {Number} options.throttle - Throttle interval in ms (default: 16ms for ~60fps)
|
|
19
|
+
* @param {Boolean} options.once - Only fire threshold callback once (default: false)
|
|
20
|
+
* @param {Function} options.callback - Called on every scroll with scrollPercent
|
|
21
|
+
* @param {Function} options.onScrollDown - Called when scrolling down
|
|
22
|
+
* @param {Function} options.onScrollUp - Called when scrolling up
|
|
23
|
+
* @param {Function} options.onEnterThreshold - Called when entering threshold zone
|
|
24
|
+
*/
|
|
25
|
+
export function scrollObserver(node, options = {}) {
|
|
26
|
+
const { callback, onScrollDown, onScrollUp, onEnterThreshold, ...userOpts } =
|
|
27
|
+
options
|
|
28
|
+
const opts = { ...defaultOptions, ...userOpts }
|
|
29
|
+
const { threshold, tolerance, throttle, once } = opts
|
|
30
|
+
|
|
31
|
+
const prevScrollDirection = null
|
|
32
|
+
let prevScrollTop = 0
|
|
33
|
+
let prevScrollPercent = 0
|
|
34
|
+
let lastThrottleTime = 0
|
|
35
|
+
let thresholdFired = false
|
|
36
|
+
let rafId = null
|
|
37
|
+
|
|
38
|
+
// Determine scroll context once at initialization
|
|
39
|
+
const isDocumentScroll = node === document || node === window
|
|
40
|
+
const scrollElement = isDocumentScroll ? document.documentElement : node
|
|
41
|
+
|
|
42
|
+
// Cache DOM references and expensive calculations
|
|
43
|
+
let cachedScrollHeight = 0
|
|
44
|
+
let cachedClientHeight = 0
|
|
45
|
+
|
|
46
|
+
// Initialize cache and start observing for changes
|
|
47
|
+
updateCache()
|
|
48
|
+
const cacheObserver = resizeObserver(scrollElement, {
|
|
49
|
+
callback: updateCache,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
node.addEventListener('scroll', throttledObserve, { passive: true })
|
|
53
|
+
|
|
54
|
+
function updateCache() {
|
|
55
|
+
cachedScrollHeight = scrollElement.scrollHeight
|
|
56
|
+
cachedClientHeight = scrollElement.clientHeight
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function throttledObserve() {
|
|
60
|
+
const now = Date.now()
|
|
61
|
+
if (now - lastThrottleTime < throttle) return
|
|
62
|
+
|
|
63
|
+
lastThrottleTime = now
|
|
64
|
+
rafId = requestAnimationFrame(observe)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function observe() {
|
|
68
|
+
const scrollTop = scrollElement.scrollTop
|
|
69
|
+
|
|
70
|
+
// Skip if scroll position hasn't changed meaningfully
|
|
71
|
+
if (Math.abs(scrollTop - prevScrollTop) < 1) return
|
|
72
|
+
|
|
73
|
+
const scrollDirection = scrollTop > prevScrollTop ? 'down' : 'up'
|
|
74
|
+
const maxScroll = Math.max(1, cachedScrollHeight - cachedClientHeight)
|
|
75
|
+
const scrollPercent = Math.min(1, Math.max(0, scrollTop / maxScroll))
|
|
76
|
+
|
|
77
|
+
// Check threshold crossing (e.g., 75% ± tolerance)
|
|
78
|
+
const thresholdMin = threshold - tolerance / 2
|
|
79
|
+
const thresholdMax = threshold + tolerance / 2
|
|
80
|
+
const wasInThreshold =
|
|
81
|
+
prevScrollPercent >= thresholdMin && prevScrollPercent <= thresholdMax
|
|
82
|
+
const isInThreshold =
|
|
83
|
+
scrollPercent >= thresholdMin && scrollPercent <= thresholdMax
|
|
84
|
+
const hasEnteredThreshold =
|
|
85
|
+
!wasInThreshold && isInThreshold && (!once || !thresholdFired)
|
|
86
|
+
|
|
87
|
+
if (hasEnteredThreshold && once) {
|
|
88
|
+
thresholdFired = true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Prepare common data for all callbacks
|
|
92
|
+
const callbackData = {
|
|
93
|
+
scrollTop,
|
|
94
|
+
scrollDirection,
|
|
95
|
+
scrollPercent,
|
|
96
|
+
directionChanged: scrollDirection !== prevScrollDirection,
|
|
97
|
+
hasEnteredThreshold,
|
|
98
|
+
isInThreshold,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Call specific callbacks
|
|
102
|
+
if (typeof callback === 'function') {
|
|
103
|
+
callback(callbackData)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Only fire direction callbacks when direction actually changes
|
|
107
|
+
if (scrollDirection !== prevScrollDirection) {
|
|
108
|
+
if (scrollDirection === 'down' && typeof onScrollDown === 'function') {
|
|
109
|
+
onScrollDown(callbackData)
|
|
110
|
+
}
|
|
111
|
+
if (scrollDirection === 'up' && typeof onScrollUp === 'function') {
|
|
112
|
+
onScrollUp(callbackData)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Fire threshold callback when entering threshold zone
|
|
117
|
+
if (hasEnteredThreshold && typeof onEnterThreshold === 'function') {
|
|
118
|
+
onEnterThreshold(callbackData)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
prevScrollTop = scrollTop
|
|
122
|
+
prevScrollPercent = scrollPercent
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
destroy() {
|
|
127
|
+
node.removeEventListener('scroll', throttledObserve)
|
|
128
|
+
if (rafId) cancelAnimationFrame(rafId)
|
|
129
|
+
cacheObserver.destroy()
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a curried version of a function that can be called with partial arguments
|
|
3
|
+
* @param {Function} fn - The function to curry
|
|
4
|
+
* @return {Function} The curried function
|
|
5
|
+
* @property {any} return - The result of calling the original function when all arguments are provided
|
|
6
|
+
* @example
|
|
7
|
+
* const add = (a, b, c) => a + b + c
|
|
8
|
+
* const curriedAdd = curry(add)
|
|
9
|
+
* curriedAdd(1)(2)(3) // 6
|
|
10
|
+
* curriedAdd(1, 2)(3) // 6
|
|
11
|
+
* curriedAdd(1)(2, 3) // 6
|
|
12
|
+
*/
|
|
1
13
|
export function curry(fn) {
|
|
2
14
|
return function curried(...args) {
|
|
3
15
|
if (args.length >= fn.length) {
|
|
@@ -10,18 +22,88 @@ export function curry(fn) {
|
|
|
10
22
|
}
|
|
11
23
|
}
|
|
12
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Composes functions from right to left (synchronous)
|
|
27
|
+
* @param {...Function} fns - Functions to compose
|
|
28
|
+
* @return {Function} The composed function
|
|
29
|
+
* @property {any} return - The result of applying all functions in sequence
|
|
30
|
+
* @example
|
|
31
|
+
* const add1 = x => x + 1
|
|
32
|
+
* const multiply2 = x => x * 2
|
|
33
|
+
* const composed = compose(add1, multiply2)
|
|
34
|
+
* composed(3) // 7 (3 * 2 + 1)
|
|
35
|
+
*/
|
|
13
36
|
export function compose(...fns) {
|
|
14
37
|
return function (value) {
|
|
15
38
|
return fns.reduceRight((acc, fn) => fn(acc), value)
|
|
16
39
|
}
|
|
17
40
|
}
|
|
18
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Composes functions from right to left (asynchronous)
|
|
44
|
+
* @param {...Function} fns - Functions to compose (can be sync or async)
|
|
45
|
+
* @return {Function} The composed async function
|
|
46
|
+
* @property {Promise<any>} return - A Promise that resolves to the result of applying all functions
|
|
47
|
+
* @example
|
|
48
|
+
* const add1 = x => x + 1
|
|
49
|
+
* const multiplyAsync = async x => x * 2
|
|
50
|
+
* await composeAsync(add1, multiplyAsync)(3) // 7 (3 * 2 + 1)
|
|
51
|
+
*/
|
|
52
|
+
export function composeAsync(...fns) {
|
|
53
|
+
return async function (value) {
|
|
54
|
+
return fns.reduceRight(async (acc, fn) => {
|
|
55
|
+
const result = await acc
|
|
56
|
+
return await fn(result)
|
|
57
|
+
}, value)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Pipes functions from left to right (synchronous)
|
|
63
|
+
* @param {...Function} fns - Functions to pipe
|
|
64
|
+
* @return {Function} The piped function
|
|
65
|
+
* @property {any} return - The result of applying all functions in sequence
|
|
66
|
+
* @example
|
|
67
|
+
* const add1 = x => x + 1
|
|
68
|
+
* const multiply2 = x => x * 2
|
|
69
|
+
* const piped = pipe(add1, multiply2)
|
|
70
|
+
* piped(3) // 8 (3 + 1) * 2
|
|
71
|
+
*/
|
|
19
72
|
export function pipe(...fns) {
|
|
20
73
|
return function (value) {
|
|
21
74
|
return fns.reduce((acc, fn) => fn(acc), value)
|
|
22
75
|
}
|
|
23
76
|
}
|
|
24
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Pipes functions from left to right (asynchronous)
|
|
80
|
+
* @param {...Function} fns - Functions to pipe (can be sync or async)
|
|
81
|
+
* @return {Function} The piped async function
|
|
82
|
+
* @property {Promise<any>} return - A Promise that resolves to the result of applying all functions
|
|
83
|
+
* @example
|
|
84
|
+
* const add1 = x => x + 1
|
|
85
|
+
* const multiplyAsync = async x => x * 2
|
|
86
|
+
* await pipeAsync(add1, multiplyAsync)(3) // 8 ((3 + 1) * 2)
|
|
87
|
+
*/
|
|
88
|
+
export function pipeAsync(...fns) {
|
|
89
|
+
return async function (value) {
|
|
90
|
+
return fns.reduce(async (acc, fn) => {
|
|
91
|
+
const result = await acc
|
|
92
|
+
return await fn(result)
|
|
93
|
+
}, value)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Calls a function n times with the current index and returns an array of results
|
|
99
|
+
* @param {Function} fn - Function to call (receives index as argument)
|
|
100
|
+
* @param {number} n - Number of times to call the function
|
|
101
|
+
* @return {any[]} Array of results from calling the function
|
|
102
|
+
* @property {any[]} return - Array containing the results of each function call
|
|
103
|
+
* @example
|
|
104
|
+
* times(i => i * 2, 3) // [0, 2, 4]
|
|
105
|
+
* times(() => Math.random(), 2) // [0.123, 0.456] (random values)
|
|
106
|
+
*/
|
|
25
107
|
export function times(fn, n) {
|
|
26
108
|
const result = []
|
|
27
109
|
for (let i = 0; i < n; i++) {
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
compose,
|
|
4
|
+
composeAsync,
|
|
5
|
+
curry,
|
|
6
|
+
pipe,
|
|
7
|
+
pipeAsync,
|
|
8
|
+
times,
|
|
9
|
+
} from './functional.js'
|
|
10
|
+
|
|
11
|
+
describe('functional utilities', () => {
|
|
12
|
+
describe('curry', () => {
|
|
13
|
+
it('should curry a function with multiple arguments', () => {
|
|
14
|
+
const add = (a, b, c) => a + b + c
|
|
15
|
+
const curriedAdd = curry(add)
|
|
16
|
+
|
|
17
|
+
expect(curriedAdd(1)(2)(3)).toBe(6)
|
|
18
|
+
expect(curriedAdd(1, 2)(3)).toBe(6)
|
|
19
|
+
expect(curriedAdd(1)(2, 3)).toBe(6)
|
|
20
|
+
expect(curriedAdd(1, 2, 3)).toBe(6)
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('compose (sync)', () => {
|
|
25
|
+
it('should compose functions from right to left', () => {
|
|
26
|
+
const add1 = x => x + 1
|
|
27
|
+
const multiply2 = x => x * 2
|
|
28
|
+
const composed = compose(add1, multiply2)
|
|
29
|
+
|
|
30
|
+
expect(composed(3)).toBe(7) // (3 * 2) + 1
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should work with single function', () => {
|
|
34
|
+
const add1 = x => x + 1
|
|
35
|
+
const composed = compose(add1)
|
|
36
|
+
|
|
37
|
+
expect(composed(5)).toBe(6)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should work with multiple functions', () => {
|
|
41
|
+
const add1 = x => x + 1
|
|
42
|
+
const multiply2 = x => x * 2
|
|
43
|
+
const subtract3 = x => x - 3
|
|
44
|
+
const composed = compose(subtract3, add1, multiply2)
|
|
45
|
+
|
|
46
|
+
expect(composed(5)).toBe(8) // ((5 * 2) + 1) - 3 = 8
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('composeAsync', () => {
|
|
51
|
+
it('should compose async functions from right to left', async () => {
|
|
52
|
+
const add1 = x => x + 1
|
|
53
|
+
const multiplyAsync = async x => x * 2
|
|
54
|
+
|
|
55
|
+
const result = await composeAsync(add1, multiplyAsync)(3)
|
|
56
|
+
expect(result).toBe(7) // (3 * 2) + 1
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should work with all sync functions', async () => {
|
|
60
|
+
const add1 = x => x + 1
|
|
61
|
+
const multiply2 = x => x * 2
|
|
62
|
+
|
|
63
|
+
const result = await composeAsync(add1, multiply2)(3)
|
|
64
|
+
expect(result).toBe(7) // (3 * 2) + 1
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should work with all async functions', async () => {
|
|
68
|
+
const add1Async = async x => x + 1
|
|
69
|
+
const multiply2Async = async x => x * 2
|
|
70
|
+
|
|
71
|
+
const result = await composeAsync(add1Async, multiply2Async)(3)
|
|
72
|
+
expect(result).toBe(7) // (3 * 2) + 1
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should work with mixed sync and async functions', async () => {
|
|
76
|
+
const add1 = x => x + 1
|
|
77
|
+
const multiply2Async = async x => x * 2
|
|
78
|
+
const subtract3 = x => x - 3
|
|
79
|
+
|
|
80
|
+
const result = await composeAsync(subtract3, add1, multiply2Async)(5)
|
|
81
|
+
expect(result).toBe(8) // ((5 * 2) + 1) - 3 = 8
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('pipe (sync)', () => {
|
|
86
|
+
it('should pipe functions from left to right', () => {
|
|
87
|
+
const add1 = x => x + 1
|
|
88
|
+
const multiply2 = x => x * 2
|
|
89
|
+
const piped = pipe(add1, multiply2)
|
|
90
|
+
|
|
91
|
+
expect(piped(3)).toBe(8) // (3 + 1) * 2
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should work with single function', () => {
|
|
95
|
+
const add1 = x => x + 1
|
|
96
|
+
const piped = pipe(add1)
|
|
97
|
+
|
|
98
|
+
expect(piped(5)).toBe(6)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should work with multiple functions', () => {
|
|
102
|
+
const add1 = x => x + 1
|
|
103
|
+
const multiply2 = x => x * 2
|
|
104
|
+
const subtract3 = x => x - 3
|
|
105
|
+
const piped = pipe(add1, multiply2, subtract3)
|
|
106
|
+
|
|
107
|
+
expect(piped(5)).toBe(9) // ((5 + 1) * 2) - 3 = 9
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe('pipeAsync', () => {
|
|
112
|
+
it('should pipe async functions from left to right', async () => {
|
|
113
|
+
const add1 = x => x + 1
|
|
114
|
+
const multiplyAsync = async x => x * 2
|
|
115
|
+
|
|
116
|
+
const result = await pipeAsync(add1, multiplyAsync)(3)
|
|
117
|
+
expect(result).toBe(8) // (3 + 1) * 2
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should work with all sync functions', async () => {
|
|
121
|
+
const add1 = x => x + 1
|
|
122
|
+
const multiply2 = x => x * 2
|
|
123
|
+
|
|
124
|
+
const result = await pipeAsync(add1, multiply2)(3)
|
|
125
|
+
expect(result).toBe(8) // (3 + 1) * 2
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('should work with all async functions', async () => {
|
|
129
|
+
const add1Async = async x => x + 1
|
|
130
|
+
const multiply2Async = async x => x * 2
|
|
131
|
+
|
|
132
|
+
const result = await pipeAsync(add1Async, multiply2Async)(3)
|
|
133
|
+
expect(result).toBe(8) // (3 + 1) * 2
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('should work with mixed sync and async functions', async () => {
|
|
137
|
+
const add1 = x => x + 1
|
|
138
|
+
const multiply2Async = async x => x * 2
|
|
139
|
+
const subtract3 = x => x - 3
|
|
140
|
+
|
|
141
|
+
const result = await pipeAsync(add1, multiply2Async, subtract3)(5)
|
|
142
|
+
expect(result).toBe(9) // ((5 + 1) * 2) - 3 = 9
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('should handle promises in the pipeline', async () => {
|
|
146
|
+
const asyncAdd = async x => {
|
|
147
|
+
await new Promise(resolve => setTimeout(resolve, 1))
|
|
148
|
+
return x + 1
|
|
149
|
+
}
|
|
150
|
+
const multiply2 = x => x * 2
|
|
151
|
+
|
|
152
|
+
const result = await pipeAsync(asyncAdd, multiply2)(3)
|
|
153
|
+
expect(result).toBe(8) // (3 + 1) * 2
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
describe('times', () => {
|
|
158
|
+
it('should call function n times with index', () => {
|
|
159
|
+
const result = times(i => i * 2, 3)
|
|
160
|
+
expect(result).toEqual([0, 2, 4])
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('should work with zero iterations', () => {
|
|
164
|
+
const result = times(i => i, 0)
|
|
165
|
+
expect(result).toEqual([])
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('should work with functions that ignore index', () => {
|
|
169
|
+
const result = times(() => 'hello', 2)
|
|
170
|
+
expect(result).toEqual(['hello', 'hello'])
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
// Edge cases and error handling
|
|
175
|
+
describe('edge cases', () => {
|
|
176
|
+
it('should handle empty function arrays in compose', () => {
|
|
177
|
+
const composed = compose()
|
|
178
|
+
expect(composed(5)).toBe(5)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('should handle empty function arrays in pipe', () => {
|
|
182
|
+
const piped = pipe()
|
|
183
|
+
expect(piped(5)).toBe(5)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('should handle empty function arrays in composeAsync', async () => {
|
|
187
|
+
const result = await composeAsync()(5)
|
|
188
|
+
expect(result).toBe(5)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('should handle empty function arrays in pipeAsync', async () => {
|
|
192
|
+
const result = await pipeAsync()(5)
|
|
193
|
+
expect(result).toBe(5)
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
})
|