@nativerent/js-utils 1.1.0 → 1.2.0

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/jest.config.mjs CHANGED
@@ -1,5 +1,9 @@
1
1
  export default {
2
2
  preset: "ts-jest",
3
3
  testEnvironment: "jsdom",
4
+ testEnvironmentOptions: {
5
+ url: "https://example.com/current%20path",
6
+ },
4
7
  testMatch: ["**/tests/**"],
8
+ setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
5
9
  };
package/jest.setup.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { TextEncoder, TextDecoder } from "util";
2
+
3
+ (global as any).TextEncoder = TextEncoder;
4
+ (global as any).TextDecoder = TextDecoder;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nativerent/js-utils",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { FlattenableObject, Primitive, SimpleObject } from "./types";
1
+ import type { Primitive, SimpleObject } from "./types";
2
2
 
3
3
  export function debounce(fn: Function, delay: number): () => void {
4
4
  let timeout: ReturnType<typeof setTimeout>;
@@ -42,7 +42,7 @@ export function isString(value: any): value is string {
42
42
  * Check if the given argument is a string which is not empty
43
43
  */
44
44
  export function isNotEmptyString(value: any) {
45
- return isString(value) && value.length;
45
+ return isString(value) && value.length > 0;
46
46
  }
47
47
 
48
48
  export function isHTMLElement(value: any): value is HTMLElement {
@@ -88,16 +88,18 @@ export function arrayIntersect(
88
88
  array1: Array<Primitive | null | undefined>,
89
89
  array2: Array<Primitive | null | undefined>,
90
90
  ) {
91
- return array1.filter((value) => array2.includes(value));
91
+ const set = new Set(array2);
92
+ return array1.filter((value) => set.has(value));
92
93
  }
93
94
 
94
95
  export function deepCloneObject<Type>(object: Type): Type {
95
96
  return JSON.parse(JSON.stringify(object));
96
97
  }
97
98
 
99
+ // todo: non-px values
98
100
  export function getNumericStyleProp(prop: string, el: Element) {
99
101
  const styles: { [key: string]: any } = window.getComputedStyle(el);
100
- return !isNullOrUndef(styles[prop]) ? parseInt(styles[prop].slice(0, -2)) : 0;
102
+ return !isNullOrUndef(styles[prop]) ? parseFloat(styles[prop] || 0) : 0;
101
103
  }
102
104
 
103
105
  export function objectHasProp(
@@ -150,58 +152,39 @@ export function objectToQueryString(object: {
150
152
  .join("&");
151
153
  }
152
154
 
153
- export function countObjectInnerLength(object: {
154
- [key: string | number]: any[];
155
- }) {
156
- const obj = Object.values(object);
157
- const len = obj.length;
158
-
159
- if (len === 0) {
160
- return 0;
161
- } else if (len === 1) {
162
- return obj[0].length;
163
- } else {
164
- return obj.reduce((acc: any, cur) => {
165
- if (Array.isArray(acc)) {
166
- acc = acc.length;
167
- }
168
- if (cur) {
169
- acc += cur.length;
170
- }
171
- return acc;
172
- });
173
- }
155
+ export function countObjectInnerLength(
156
+ object: Record<string | number, any[] | null | undefined>,
157
+ ) {
158
+ return Object.values(object).reduce(
159
+ (sum, arr) => sum + (arr?.length ?? 0),
160
+ 0,
161
+ );
174
162
  }
175
163
 
176
164
  /**
177
165
  * Check if the element is in viewport
178
166
  */
179
- export function isElementVisible(element: HTMLElement, minVisiblePercent = 50) {
180
- const elementCoords = element.getBoundingClientRect();
181
- const elementHeight = elementCoords.height;
182
-
183
- if (!elementHeight) {
184
- return false;
185
- }
167
+ export function isElementVisible(
168
+ element: HTMLElement,
169
+ minVisiblePercent = 50,
170
+ ): boolean {
171
+ const { top, bottom, height } = element.getBoundingClientRect();
172
+ if (!height) return false;
186
173
 
187
- const elementTop = elementCoords.top;
188
- const elementBottom = elementCoords.bottom;
189
- const viewPortHeight = window.innerHeight;
174
+ const viewportTop = 0;
175
+ const viewportBottom = window.innerHeight;
190
176
 
191
- const elementVisibleHeight =
192
- elementTop > 0
193
- ? // the element is in or below viewport
194
- viewPortHeight - elementTop
195
- : // the element is still in or above viewport
196
- elementBottom;
177
+ const visibleTop = Math.max(top, viewportTop);
178
+ const visibleBottom = Math.min(bottom, viewportBottom);
179
+ const visibleHeight = Math.max(0, visibleBottom - visibleTop);
197
180
 
198
- return (elementVisibleHeight / elementHeight) * 100 >= minVisiblePercent;
181
+ return (visibleHeight / height) * 100 >= minVisiblePercent;
199
182
  }
200
183
 
201
184
  export function decodeSafeURL(url: string): string {
202
185
  try {
203
186
  return decodeURI(url);
204
- } catch (e) {
187
+ } catch {
205
188
  return url;
206
189
  }
207
190
  }
@@ -240,12 +223,14 @@ export function injectStyleLink(filename: string): void {
240
223
  document.head.appendChild(link);
241
224
  }
242
225
 
243
- export function toBinaryStr(str: string) {
244
- const encoder = new TextEncoder();
245
- // 1: split the UTF-16 string into an array of bytes
246
- const charCodes = encoder.encode(str);
247
- // 2: concatenate byte data to create a binary string
248
- return String.fromCharCode(...charCodes);
226
+ export function toBinaryStr(str: string): string {
227
+ const CHUNK = 0x8000; // String.fromCharCode(...charCodes) can overflow the call stack with large strings
228
+ const bytes = new TextEncoder().encode(str);
229
+ let result = "";
230
+ for (let i = 0; i < bytes.length; i += CHUNK) {
231
+ result += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
232
+ }
233
+ return result;
249
234
  }
250
235
 
251
236
  /**
@@ -269,18 +254,11 @@ export function stringToHtmlElements(html: string): NodeListOf<ChildNode> {
269
254
  */
270
255
  export function createHtmlElement(
271
256
  type: string,
272
- attributes: { [key: string]: string } = {},
257
+ attributes: Record<string, string> = {},
273
258
  ): HTMLElement {
274
- const attrs = getObjectKeys(attributes);
275
- const element = document.createElement(type);
276
-
277
- if (attrs.length) {
278
- attrs.forEach((name) => {
279
- element.setAttribute(name, attributes[name]);
280
- });
281
- }
282
-
283
- return element;
259
+ const el = document.createElement(type);
260
+ for (const [k, v] of Object.entries(attributes)) el.setAttribute(k, v);
261
+ return el as HTMLElement;
284
262
  }
285
263
 
286
264
  /**
@@ -288,67 +266,69 @@ export function createHtmlElement(
288
266
  */
289
267
  export function insertHtmlElements(
290
268
  nodes: NodeListOf<Node>,
291
- appendTo?: Element | ShadowRoot,
269
+ appendTo: Element | ShadowRoot = document.body,
292
270
  ) {
293
- appendTo = appendTo || document.body;
294
-
295
- const fragment = new DocumentFragment();
271
+ const fragment = document.createDocumentFragment();
296
272
 
297
273
  for (let i = 0; i < nodes.length; i++) {
298
- // skip non tags and non text, 3 for text nodes
299
- if (!isHTMLElement(nodes[i]) && nodes[i].nodeType !== 3) {
274
+ const node = nodes[i];
275
+
276
+ // Allow elements + text nodes only
277
+ if (!isHTMLElement(node) && node.nodeType !== Node.TEXT_NODE) {
300
278
  continue;
301
279
  }
302
280
 
303
- const node = nodes[i] as HTMLElement;
304
- let newNode;
281
+ let newNode: Node;
305
282
 
306
- switch (node.nodeName.toLowerCase()) {
307
- case "script":
308
- const src = node.getAttribute("src");
283
+ // Text node
284
+ if (node.nodeType === Node.TEXT_NODE) {
285
+ newNode = document.createTextNode(node.textContent ?? "");
286
+ fragment.appendChild(newNode);
287
+ continue;
288
+ }
309
289
 
310
- if (!isNullOrUndef(src) && src.length > 0) {
311
- newNode = createScriptElement(
312
- src,
313
- false,
314
- flatHtmlAttributes(node.attributes),
315
- );
316
- } else {
317
- newNode = createScriptElement(
318
- node.innerHTML,
319
- true,
320
- flatHtmlAttributes(node.attributes),
321
- );
322
- }
290
+ // Element node from here on
291
+ const el = node as Element;
292
+ const tag = el.tagName.toLowerCase();
293
+
294
+ switch (tag) {
295
+ case "script": {
296
+ const src = el.getAttribute("src");
297
+ newNode =
298
+ !isNullOrUndef(src) && src.length > 0
299
+ ? createScriptElement(src, false, flatHtmlAttributes(el.attributes))
300
+ : createScriptElement(
301
+ el.textContent ?? "",
302
+ true,
303
+ flatHtmlAttributes(el.attributes),
304
+ );
323
305
  break;
306
+ }
324
307
 
325
308
  case "style":
326
- newNode = createStyleElement(node.textContent);
309
+ newNode = createStyleElement(el.textContent);
327
310
  break;
328
311
 
329
312
  case "svg":
330
313
  newNode = createSvgElement(
331
- node.innerHTML,
332
- flatHtmlAttributes(node.attributes),
314
+ el.innerHTML,
315
+ flatHtmlAttributes(el.attributes),
333
316
  );
334
317
  break;
335
318
 
336
- case "#text":
337
- newNode = document.createTextNode(node.textContent ?? "");
338
- break;
339
-
340
- default:
341
- newNode = createHtmlElement(
342
- node.tagName,
343
- flatHtmlAttributes(node.attributes),
319
+ default: {
320
+ const created = createHtmlElement(
321
+ el.tagName,
322
+ flatHtmlAttributes(el.attributes),
344
323
  );
345
-
346
- // recursive
347
- if (node.childNodes.length) {
348
- insertHtmlElements(node.childNodes, newNode);
324
+ // Recurse for children (handles mixed element/text)
325
+ if (el.childNodes.length) {
326
+ insertHtmlElements(el.childNodes, created);
349
327
  } else {
350
- newNode.innerHTML = node.innerHTML;
328
+ created.textContent = el.textContent ?? "";
351
329
  }
330
+ newNode = created;
331
+ }
352
332
  }
353
333
 
354
334
  fragment.appendChild(newNode);
@@ -360,20 +340,21 @@ export function insertHtmlElements(
360
340
  /**
361
341
  * Make a simple object which contains only attribute name and value
362
342
  */
363
- export function flatHtmlAttributes(attributes?: NamedNodeMap) {
364
- let flatAttributes: { [key: string]: string } = {};
343
+ export function flatHtmlAttributes(
344
+ attributes?: NamedNodeMap | null,
345
+ ): Record<string, string> {
346
+ if (!attributes) return {};
365
347
 
366
- if (!isNullOrUndef(attributes)) {
367
- for (let i = 0; i < attributes.length; i++) {
368
- let attr = attributes[i];
348
+ const result: Record<string, string> = {};
369
349
 
370
- if (!isUndef(attr)) {
371
- flatAttributes[attr.name] = attr.value;
372
- }
350
+ for (let i = 0; i < attributes.length; i++) {
351
+ const attr = attributes[i];
352
+ if (attr) {
353
+ result[attr.name] = attr.value;
373
354
  }
374
355
  }
375
356
 
376
- return flatAttributes;
357
+ return result;
377
358
  }
378
359
 
379
360
  /**
@@ -381,33 +362,30 @@ export function flatHtmlAttributes(attributes?: NamedNodeMap) {
381
362
  */
382
363
  export function createScriptElement(
383
364
  js: string,
384
- inline: boolean = false,
385
- attributes: { [key: string]: string } = {},
386
- ) {
387
- const attrs = isObject(attributes) ? getObjectKeys(attributes) : [];
388
- const element = createHtmlElement("script") as HTMLScriptElement;
365
+ inline = false,
366
+ attributes: Record<string, string> = {},
367
+ ): HTMLScriptElement {
368
+ const el = document.createElement("script");
389
369
 
390
- if (attrs.length) {
391
- attrs.forEach((name) => {
392
- element.setAttribute(name, attributes[name]);
393
- });
370
+ for (const [k, v] of Object.entries(attributes)) {
371
+ el.setAttribute(k, v);
394
372
  }
395
373
 
396
374
  if (inline) {
397
- element.appendChild(document.createTextNode(js));
375
+ el.text = js;
398
376
  } else {
399
- element.async = true;
400
- element.src = js;
377
+ el.async = true;
378
+ el.src = js;
401
379
  }
402
380
 
403
- return element;
381
+ return el;
404
382
  }
405
383
 
406
384
  /**
407
385
  * Create a <style> element
408
386
  */
409
- export function createStyleElement(css: string | null) {
410
- const element = createHtmlElement("style");
387
+ export function createStyleElement(css: string | null): HTMLStyleElement {
388
+ const element = createHtmlElement("style") as HTMLStyleElement;
411
389
 
412
390
  if (css) {
413
391
  element.appendChild(document.createTextNode(css));
@@ -419,7 +397,7 @@ export function createStyleElement(css: string | null) {
419
397
  /**
420
398
  * Create a <link> element
421
399
  */
422
- export function createLinkElement(href: string) {
400
+ export function createLinkElement(href: string): HTMLLinkElement {
423
401
  const element = createHtmlElement("link") as HTMLLinkElement;
424
402
 
425
403
  element.rel = "stylesheet";
@@ -433,18 +411,18 @@ export function createLinkElement(href: string) {
433
411
  */
434
412
  export function createSvgElement(
435
413
  content: string,
436
- attributes: { [key: string]: string },
437
- ) {
438
- const attrs = getObjectKeys(attributes);
439
- const element = document.createElementNS("http://www.w3.org/2000/svg", "svg");
440
-
441
- if (attrs.length) {
442
- attrs.forEach((name) => {
443
- element.setAttribute(name, attributes[name]);
444
- });
414
+ attributes: Record<string, string> = {},
415
+ ): SVGSVGElement {
416
+ const element = document.createElementNS(
417
+ "http://www.w3.org/2000/svg",
418
+ "svg",
419
+ ) as SVGSVGElement;
420
+
421
+ for (const [name, value] of Object.entries(attributes)) {
422
+ element.setAttribute(name, value);
445
423
  }
446
424
 
447
- element.innerHTML = content;
425
+ element.innerHTML = content.trim();
448
426
 
449
427
  return element;
450
428
  }
@@ -455,9 +433,12 @@ export function createSvgElement(
455
433
  export function createPixelElement(src: string) {
456
434
  const image = document.createElement("img");
457
435
 
436
+ image.alt = "";
458
437
  image.src = src;
459
438
  image.width = 1;
460
439
  image.height = 1;
440
+ image.loading = "eager";
441
+ image.setAttribute("aria-hidden", "true");
461
442
  image.style.position = "absolute";
462
443
  image.style.left = "-99999px";
463
444
 
@@ -490,29 +471,23 @@ export function insertCss(css: string, className?: string, external = false) {
490
471
  * Get screen sizes
491
472
  */
492
473
  export function getScreenSize(): { width: number; height: number } {
493
- let width, height;
494
-
495
- if (window.screen) {
496
- width = screen.width;
497
- height = screen.height;
498
- } else {
499
- width = window.innerWidth;
500
- height = window.innerHeight;
501
- }
474
+ const width = window.screen?.width ?? window.innerWidth;
475
+ const height = window.screen?.height ?? window.innerHeight;
502
476
 
503
- return {
504
- width: width,
505
- height: height,
506
- };
477
+ return { width, height };
507
478
  }
508
479
 
509
480
  /**
510
481
  * Get a random integer between min and max
511
482
  */
512
- export function getRandomInt(min = 0, max = 4294967295) {
483
+ export function getRandomInt(min = 0, max = 4294967295): number {
513
484
  min = Math.ceil(min);
514
485
  max = Math.floor(max);
515
486
 
487
+ if (max <= min) {
488
+ return min;
489
+ }
490
+
516
491
  return Math.floor(Math.random() * (max - min)) + min;
517
492
  }
518
493
 
@@ -535,7 +510,10 @@ export function hashCode(str: string) {
535
510
  /**
536
511
  * Executes a callback as soon as DOM is interactive
537
512
  */
538
- export function onDOMReady(callback: Function, ...args: any[]) {
513
+ export function onDOMReady<T extends unknown[]>(
514
+ callback: (...args: T) => void,
515
+ ...args: T
516
+ ): void {
539
517
  if (
540
518
  document.readyState === "interactive" ||
541
519
  document.readyState === "complete"
@@ -693,54 +671,55 @@ export function fireInputEvent(
693
671
  export function fireEvent(
694
672
  eventName: string,
695
673
  element: string | Element | null,
696
- delay: number = 0,
674
+ delay = 0,
697
675
  ): void {
698
676
  setTimeout(() => {
699
- if (isString(element)) {
700
- element = document.querySelector(element);
701
- }
702
-
703
- if (!isNullOrUndef(element)) {
704
- element.dispatchEvent(
705
- new Event(eventName, {
706
- bubbles: true,
707
- }),
708
- );
677
+ const target = isString(element)
678
+ ? document.querySelector(element)
679
+ : element;
680
+ if (target) {
681
+ target.dispatchEvent(new Event(eventName, { bubbles: true }));
709
682
  }
710
683
  }, delay);
711
684
  }
712
685
 
713
686
  export function hasObjectChanged(
714
- object: { [key: string]: any },
715
- values: any[],
687
+ oldObj: Record<string, any>,
688
+ values: [string, any][],
689
+ checkNewKeys = false,
716
690
  ): boolean {
717
- let hasChanged = false;
691
+ const newMap = new Map(values);
718
692
 
719
- for (const key in object) {
720
- let oldVal = object[key];
721
- let [, newVal] = values.find(([newKey]) => key === newKey);
693
+ // check keys from old
694
+ for (const key of Object.keys(oldObj)) {
695
+ let oldVal = oldObj[key];
696
+ let newVal = newMap.get(key);
722
697
 
723
698
  if (isObject(oldVal)) {
724
- if (!isObject(newVal)) {
725
- hasChanged = true;
726
- } else {
727
- hasChanged = hasObjectChanged(oldVal, Object.entries(newVal));
728
- }
729
- } else if (Array.isArray(newVal) || Array.isArray(oldVal)) {
730
- newVal = Array.isArray(newVal) ? newVal : [];
731
- oldVal = Array.isArray(oldVal) ? oldVal : [];
699
+ if (!isObject(newVal)) return true;
700
+ if (hasObjectChanged(oldVal, Object.entries(newVal), checkNewKeys))
701
+ return true;
702
+ continue;
703
+ }
732
704
 
733
- hasChanged = arrayDiff(newVal, oldVal).length > 0;
734
- } else {
735
- hasChanged = areDiff(newVal, oldVal);
705
+ if (Array.isArray(oldVal) || Array.isArray(newVal)) {
706
+ const oldArr = Array.isArray(oldVal) ? oldVal : [];
707
+ const newArr = Array.isArray(newVal) ? newVal : [];
708
+ if (arrayDiff(newArr, oldArr).length > 0) return true;
709
+ continue;
736
710
  }
737
711
 
738
- if (hasChanged) {
739
- break;
712
+ if (areDiff(newVal, oldVal)) return true;
713
+ }
714
+
715
+ // optional: check keys that exist only in new
716
+ if (checkNewKeys) {
717
+ for (const key of newMap.keys()) {
718
+ if (!(key in oldObj)) return true;
740
719
  }
741
720
  }
742
721
 
743
- return hasChanged;
722
+ return false;
744
723
  }
745
724
 
746
725
  export function roundBigNum(number: number): number {
@@ -759,15 +738,15 @@ export function roundBigNum(number: number): number {
759
738
  return roundTo ? Math.round(number / roundTo) * roundTo : number;
760
739
  }
761
740
 
762
- export function extractNumbers(value: any): string {
763
- if (!isStr(value) && !isNum(value)) {
741
+ export function extractNumbers(value: unknown): string {
742
+ if (!isString(value) && !isNum(value)) {
764
743
  return "";
765
744
  }
766
745
  return value.toString().replace(/[^0-9]/g, "");
767
746
  }
768
747
 
769
- export function removeSpaces(value: any): string {
770
- if (!isStr(value) && !isNum(value)) {
748
+ export function removeSpaces(value: unknown): string {
749
+ if (!isString(value) && !isNum(value)) {
771
750
  return "";
772
751
  }
773
752
  return value.toString().replace(/\s/g, "");
@@ -802,28 +781,38 @@ export function areDiff(
802
781
  return false;
803
782
  }
804
783
 
805
- export function flattenObject(obj: FlattenableObject, prefix = "") {
806
- return Object.keys(obj).reduce((acc, k) => {
807
- const pre = prefix.length ? prefix + "." : "";
784
+ export function flattenObject(
785
+ obj: Record<string, any>,
786
+ prefix = "",
787
+ ): Record<string, any> {
788
+ const pre = prefix ? `${prefix}.` : "";
789
+
790
+ return Object.keys(obj).reduce<Record<string, any>>((acc, k) => {
791
+ const val = obj[k];
792
+ const key = `${pre}${k}`;
793
+
808
794
  if (
809
- (isObject(obj[k]) || Array.isArray(obj[k])) &&
795
+ (isObject(val) || Array.isArray(val)) &&
810
796
  getObjectKeys(obj[k]).length > 0
811
797
  ) {
812
- Object.assign(acc, flattenObject(obj[k], pre + k));
798
+ Object.assign(acc, flattenObject(val, key));
813
799
  } else {
814
- acc[pre + k] = obj[k];
800
+ acc[key] = val;
815
801
  }
816
802
  return acc;
817
- }, {} as FlattenableObject);
803
+ }, {});
818
804
  }
819
805
 
820
- export function flattenObjectAsArray(obj: FlattenableObject) {
821
- const result: FlattenableObject = {};
806
+ export function flattenObjectAsArray(
807
+ obj: Record<string, any>,
808
+ ): Record<string, any> {
822
809
  const flat = flattenObject(obj);
823
- Object.keys(flat).forEach((key) => {
824
- const newKey = key.replaceAll(/\.([^.]+)/g, "[$1]");
825
- result[newKey] = flat[key];
826
- });
810
+ const result: Record<string, any> = {};
811
+
812
+ for (const key of Object.keys(flat)) {
813
+ const newKey = key.replace(/\.([^.]+)/g, "[$1]");
814
+ result[newKey] = (flat as any)[key];
815
+ }
827
816
 
828
817
  return result;
829
818
  }
package/src/types.d.ts CHANGED
@@ -1,5 +1,3 @@
1
- export type FlattenableObject = { [key: string]: any };
2
-
3
1
  export type Primitive = boolean | number | string;
4
2
 
5
3
  export type SimpleObject = {