Skip to content

Commit 1c3c873

Browse files
Merge pull request #27 from wintondeshong/master
Port ScrollUtils, CollectionUtils#removeElementAt and StringUtils#join
2 parents 7c10b1c + be3f9a6 commit 1c3c873

9 files changed

Lines changed: 240 additions & 5 deletions

File tree

src/constants/email-constants.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* These values are from the RFC-5231 Email specification
3+
*/
4+
const EmailConstants = {
5+
addressMaxLength: 250,
6+
subjectMaxLength: 78,
7+
};
8+
9+
export { EmailConstants };

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// #region Constants
33
// -----------------------------------------------------------------------------------------
44

5+
export { EmailConstants } from "./constants/email-constants";
56
export { Rfc4646LanguageCodes } from "./constants/rfc4646-language-codes";
67
export { VideoResolutions } from "./constants/video-resolutions";
78

@@ -37,6 +38,7 @@ export { KeyValuePair } from "./interfaces/key-value-pair";
3738
export { PagedResult } from "./interfaces/paged-result";
3839
export { Result } from "./interfaces/result";
3940
export { ResultError } from "./interfaces/result-error";
41+
export { ScrollOptions } from "./interfaces/scroll-options";
4042
export { ServiceResponse } from "./interfaces/service-response";
4143

4244
// #endregion Interfaces
@@ -59,6 +61,7 @@ export { EnvironmentUtils } from "./utilities/environment-utils";
5961
export { LocalizationUtils } from "./utilities/localization-utils";
6062
export { PromiseFactory } from "./utilities/promise-factory";
6163
export { RouteUtils } from "./utilities/route-utils";
64+
export { ScrollUtils } from "./utilities/scroll-utils";
6265
export { ServiceUtils } from "./utilities/service-utils";
6366
export { StringUtils } from "./utilities/string-utils";
6467

src/interfaces/scroll-options.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface ScrollOptions extends ScrollIntoViewOptions {
2+
initialDelay?: number;
3+
}

src/utilities/collection-utils.test.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ describe("CollectionUtils", () => {
326326
type TestType = { id: number };
327327
const selector = (t: TestType) => t.id;
328328

329-
it("When collections are different lengths, then returns false", () => {
329+
it("when collections are different lengths, then returns false", () => {
330330
// Arrange
331331
const arr1: Array<TestType> = [{ id: 1 }, { id: 2 }];
332332
const arr2: Array<TestType> = [{ id: 1 }];
@@ -338,7 +338,7 @@ describe("CollectionUtils", () => {
338338
expect(result).toBe(false);
339339
});
340340

341-
it("When one of the collections is null, then returns false", () => {
341+
it("when one of the collections is null, then returns false", () => {
342342
// Arrange
343343
const arr1: Array<TestType> = [{ id: 1 }, { id: 2 }];
344344

@@ -349,7 +349,7 @@ describe("CollectionUtils", () => {
349349
expect(result).toBe(false);
350350
});
351351

352-
it("When both collections are null, then returns true", () => {
352+
it("when both collections are null, then returns true", () => {
353353
// Act
354354
const result = CollectionUtils.equalsBy(
355355
selector,
@@ -361,7 +361,7 @@ describe("CollectionUtils", () => {
361361
expect(result).toBe(true);
362362
});
363363

364-
it("When collections are equal size but contain different elements, then returns false", () => {
364+
it("when collections are equal size but contain different elements, then returns false", () => {
365365
// Arrange
366366
const arr1: Array<TestType> = [{ id: 1 }, { id: 2 }];
367367
const arr2: Array<TestType> = [{ id: 2 }, { id: 3 }];
@@ -373,7 +373,7 @@ describe("CollectionUtils", () => {
373373
expect(result).toBe(false);
374374
});
375375

376-
it("When collections are identical, then returns true", () => {
376+
it("when collections are identical, then returns true", () => {
377377
// Arrange
378378
const arr1: Array<TestType> = [{ id: 1 }, { id: 2 }];
379379
const arr2: Array<TestType> = [...arr1];
@@ -386,6 +386,45 @@ describe("CollectionUtils", () => {
386386
});
387387
});
388388

389+
describe("#removeElementAt", () => {
390+
it("when index i < 0, returns source array", () => {
391+
// Arrange
392+
const arr = ["one", "two", "three"];
393+
const expected = [...arr];
394+
395+
// Act
396+
const result = CollectionUtils.removeElementAt(arr, -1);
397+
398+
// Assert
399+
expect(result).toStrictEqual(expected);
400+
});
401+
402+
it("when index > array.length, returns source array", () => {
403+
// Arrange
404+
const arr = ["one", "two", "three"];
405+
const expected = [...arr];
406+
407+
// Act
408+
const result = CollectionUtils.removeElementAt(arr, 50);
409+
410+
// Assert
411+
expect(result).toStrictEqual(expected);
412+
});
413+
414+
it("when index is in range, then removes element at index", () => {
415+
// Arrange
416+
const arr = ["one", "two", "three"];
417+
const expected = ["one", "three"];
418+
const indexToRemove = 1;
419+
420+
// Act
421+
const result = CollectionUtils.removeElementAt(arr, indexToRemove);
422+
423+
// Assert
424+
expect(result).toStrictEqual(expected);
425+
});
426+
});
427+
389428
describe("#replaceElementAt", () => {
390429
it("Replaces element at specified index and returns a new array", () => {
391430
// Arrange

src/utilities/collection-utils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,21 @@ const _length = (arr: Array<any> | List<any>): number => {
138138
return (arr as Array<any>).length;
139139
};
140140

141+
/**
142+
* Removes a supplied element by index
143+
* @param source original array
144+
* @param index array index to remove
145+
*/
146+
const _removeElementAt = <T>(source: Array<T>, index: number): Array<T> => {
147+
if (index < 0 || index > source.length) {
148+
return source;
149+
}
150+
151+
const newArr = [...source];
152+
newArr.splice(index, 1);
153+
return newArr;
154+
};
155+
141156
/**
142157
* Returns a NEW array with the element at the specified index
143158
* replaced with the specified value. Since it returns a new array,
@@ -181,6 +196,7 @@ export const CollectionUtils = {
181196
isEmpty: _isEmpty,
182197
isNotEmpty: _isNotEmpty,
183198
length: _length,
199+
removeElementAt: _removeElementAt,
184200
replaceElementAt: _replaceElementAt,
185201
sample: _.sample,
186202
sampleSize: _.sampleSize,

src/utilities/scroll-utils.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
describe("ScrollUtils", () => {
2+
test.skip("TODO: Backfill tests for issue https://github.com/AndcultureCode/AndcultureCode.JavaScript.Core/issues/26", () => {});
3+
});

src/utilities/scroll-utils.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { EnvironmentUtils } from "./environment-utils";
2+
import { ScrollOptions } from "../interfaces/scroll-options";
3+
import { StringUtils } from "./string-utils";
4+
5+
// -----------------------------------------------------------------------------------------
6+
// #region Constants
7+
// -----------------------------------------------------------------------------------------
8+
9+
export const DefaultScrollOptions: ScrollOptions = {
10+
behavior: "auto",
11+
block: "start",
12+
inline: "nearest",
13+
};
14+
15+
// #endregion Constants
16+
17+
// -----------------------------------------------------------------------------------------
18+
// #region Functions
19+
// -----------------------------------------------------------------------------------------
20+
21+
/**
22+
* Attempts to scroll to the element specified by the given ID.
23+
* In the event of a slow page render, the element may not be immediately available.
24+
* This method will retry up to 50 times every 100ms to find the element before
25+
* giving up.
26+
*/
27+
const _scrollToElementById = (
28+
id: string,
29+
options: ScrollOptions = DefaultScrollOptions
30+
) => {
31+
let retryCount = 0;
32+
const tryToScroll = () => {
33+
retryCount += 1;
34+
35+
if (retryCount > 50) {
36+
EnvironmentUtils.runIfDevelopment(() =>
37+
console.warn(
38+
`Could not find element with ID ${id} in the page.`
39+
)
40+
);
41+
42+
// couldn't find element in 50 loops, give up.
43+
return;
44+
}
45+
46+
const el = document.getElementById(id);
47+
if (el != null) {
48+
el.scrollIntoView(options);
49+
return;
50+
}
51+
52+
setTimeout(tryToScroll, 100);
53+
};
54+
55+
if (options.initialDelay != null) {
56+
setTimeout(tryToScroll, options.initialDelay);
57+
return;
58+
}
59+
60+
tryToScroll();
61+
};
62+
63+
/**
64+
* Attempts to scroll to the element specified in the hash of the current path.
65+
* In the event of a slow page render, the element may not be immediately available.
66+
* This method will retry up to 50 times every 100ms to find the element before
67+
* giving up.
68+
*
69+
* Reference:
70+
* https://stackoverflow.com/a/54042987
71+
* https://stackoverflow.com/a/48195222
72+
*/
73+
const _scrollToHash = (
74+
location: any,
75+
options: ScrollOptions = DefaultScrollOptions
76+
) => {
77+
if (StringUtils.isEmpty(location.hash)) {
78+
return;
79+
}
80+
81+
const id = location.hash.replace("#", "");
82+
_scrollToElementById(id, options);
83+
};
84+
85+
// #endregion Functions
86+
87+
// -----------------------------------------------------------------------------------------
88+
// #region Exports
89+
// -----------------------------------------------------------------------------------------
90+
91+
export const ScrollUtils = {
92+
scrollToHash: _scrollToHash,
93+
};
94+
95+
// #endregion Exports

src/utilities/string-utils.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,55 @@ describe("StringUtils", () => {
114114

115115
// #endregion isValidEmail
116116

117+
// -----------------------------------------------------------------------------------------
118+
// #region join()
119+
// -----------------------------------------------------------------------------------------
120+
121+
describe("join()", () => {
122+
describe("with the default separator", () => {
123+
type JoinTestTypes = [string[], string];
124+
125+
test.each<JoinTestTypes>([
126+
[[], ""],
127+
[["a"], "a"],
128+
[["a"], "a"],
129+
[["a", "b"], "a,b"],
130+
])(
131+
"when values is %p, returns %p",
132+
(values: string[], expected: string) => {
133+
// Arrange & Act
134+
const result: string = StringUtils.join(values);
135+
136+
// Assert
137+
expect(result).toBe(expected);
138+
}
139+
);
140+
});
141+
142+
describe("with a separator argument", () => {
143+
type JoinTestTypesWithSeparator = [string[], string, string];
144+
145+
test.each<JoinTestTypesWithSeparator>([
146+
[[], "", ""],
147+
[["a"], "", "a"],
148+
[["a"], ",", "a"],
149+
[["a", "b"], "", "ab"],
150+
[["a", "b"], " ", "a b"],
151+
])(
152+
"when values is %p and separator is %p, returns %p",
153+
(values: string[], separator: string, expected: string) => {
154+
// Arrange & Act
155+
const result: string = StringUtils.join(values, separator);
156+
157+
// Assert
158+
expect(result).toBe(expected);
159+
}
160+
);
161+
});
162+
});
163+
164+
// #endregion join
165+
117166
// -----------------------------------------------------------------------------------------
118167
// #region truncateRight()
119168
// -----------------------------------------------------------------------------------------

src/utilities/string-utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { CollectionUtils } from "./collection-utils";
12
import _ from "lodash";
23

34
// -----------------------------------------------------------------------------------------
@@ -48,6 +49,22 @@ const isEmpty = (value?: string): boolean =>
4849
const isValidEmail = (value?: string): boolean =>
4950
value != null && REGEX_VALID_EMAIL.test(value);
5051

52+
/**
53+
* Joins an array of strings into one string with a separator. If the array is empty, it will return an empty string.
54+
*
55+
* @default ""
56+
* @param {string[]} values Values to join into one string.
57+
* @param {string} [separator=","] String to seperate each of the given values.
58+
* @returns {string}
59+
*/
60+
const join = (values: string[], separator: string = ","): string => {
61+
if (CollectionUtils.isEmpty(values)) {
62+
return "";
63+
}
64+
65+
return values.join(separator);
66+
};
67+
5168
const truncateRight = (value: string, truncateAtPos: number): string => {
5269
if (value.length <= truncateAtPos) {
5370
return value;
@@ -73,6 +90,7 @@ export const StringUtils = {
7390
hasValue,
7491
isEmpty,
7592
isValidEmail,
93+
join,
7694
lowerFirst: _.lowerFirst,
7795
pad: _.pad,
7896
padEnd: _.padEnd,

0 commit comments

Comments
 (0)