@nativerent/js-utils 1.0.0 → 1.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nativerent/js-utils",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -12,11 +12,15 @@
12
12
  "author": "NativeRent",
13
13
  "license": "GPL-3.0-or-later",
14
14
  "devDependencies": {
15
+ "@types/jest": "^29.5.11",
15
16
  "eslint": "^8.56.0",
17
+ "jest": "^29.7.0",
16
18
  "prettier": "^3.1.1",
17
19
  "rollup": "^4.9.4",
18
20
  "rollup-plugin-dts": "^6.1.0",
19
21
  "rollup-plugin-esbuild": "^6.1.0",
20
- "typescript": "^5.3.3"
22
+ "ts-jest": "^29.1.1",
23
+ "typescript": "^5.3.3",
24
+ "jest-environment-jsdom": "^29.7.0"
21
25
  }
22
26
  }
package/src/index.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { Primitive } from "./types";
1
+ import { FlattenableObject, Primitive, SimpleObject } from "./types";
2
2
 
3
3
  export function debounce(fn: Function, delay: number): () => void {
4
4
  let timeout: ReturnType<typeof setTimeout>;
5
5
 
6
- return function (this: any, ...args: any[]) {
6
+ return function (...args: any[]) {
7
7
  clearTimeout(timeout);
8
- timeout = setTimeout(() => fn.apply(this, args), delay);
8
+ timeout = setTimeout(() => fn.apply(null, args), delay);
9
9
  };
10
10
  }
11
11
 
@@ -38,6 +38,13 @@ export function isString(str: any): str is string {
38
38
  return typeof str === "string";
39
39
  }
40
40
 
41
+ /**
42
+ * Check if the given argument is a string which is not empty
43
+ */
44
+ export function isNotEmptyString(str: any) {
45
+ return typeof str === "string" && str.length;
46
+ }
47
+
41
48
  export function isHTMLElement(el: any): el is HTMLElement {
42
49
  return el instanceof HTMLElement || el instanceof SVGElement;
43
50
  }
@@ -117,23 +124,75 @@ export function getObjectKeys(object: object): string[] {
117
124
  }
118
125
 
119
126
  /**
120
- * Works with primitive objects, see JSDoc @param
121
- *
122
- * @param object {[key: string]: SimpleObject | { [key: string]: SimpleObject }}
127
+ * Convert an object to a query string
128
+ * Works with primitive objects
123
129
  */
124
- export function objectToQueryString(object: object): string {
130
+ export function objectToQueryString(object: {
131
+ [key: string | number]:
132
+ | Primitive
133
+ | Array<Primitive | null | undefined>
134
+ | SimpleObject
135
+ | { [key: string | number]: SimpleObject }
136
+ | undefined
137
+ | null;
138
+ }): string {
125
139
  return Object.entries(object)
126
140
  .map(([k, v]) => {
127
141
  if (Array.isArray(v)) {
128
- return v.map((item) => `${k}[]=${encodeURIComponent(item)}`).join("&");
142
+ return v
143
+ .map((item) => `${k}[]=${encodeURIComponent(item || "")}`)
144
+ .join("&");
129
145
  } else if (isObject(v)) {
130
- v = JSON.stringify(v);
146
+ return `${k}=${encodeURIComponent(JSON.stringify(v))}`;
131
147
  }
132
148
  return `${k}=${encodeURIComponent(v || "")}`;
133
149
  })
134
150
  .join("&");
135
151
  }
136
152
 
153
+ export function countObjectInnerLength(object: {
154
+ [key: string | number]: any;
155
+ }) {
156
+ const obj = Object.values(object);
157
+
158
+ return obj.length === 1
159
+ ? obj[0].length
160
+ : obj.reduce((acc, cur) => {
161
+ if (Array.isArray(acc)) {
162
+ acc = acc.length;
163
+ }
164
+ if (cur) {
165
+ acc += cur.length;
166
+ }
167
+ return acc;
168
+ });
169
+ }
170
+
171
+ /**
172
+ * Check if the element is in viewport
173
+ */
174
+ export function isElementVisible(element: HTMLElement, minVisiblePercent = 50) {
175
+ const elementCoords = element.getBoundingClientRect();
176
+ const elementHeight = elementCoords.height;
177
+
178
+ if (!elementHeight) {
179
+ return false;
180
+ }
181
+
182
+ const elementTop = elementCoords.top;
183
+ const elementBottom = elementCoords.bottom;
184
+ const viewPortHeight = window.innerHeight;
185
+
186
+ const elementVisibleHeight =
187
+ elementTop > 0
188
+ ? // the element is in or below viewport
189
+ viewPortHeight - elementTop
190
+ : // the element is still in or above viewport
191
+ elementBottom;
192
+
193
+ return (elementVisibleHeight / elementHeight) * 100 >= minVisiblePercent;
194
+ }
195
+
137
196
  export function decodeSafeURL(url: string): string {
138
197
  try {
139
198
  return decodeURI(url);
@@ -142,7 +201,8 @@ export function decodeSafeURL(url: string): string {
142
201
  }
143
202
  }
144
203
 
145
- export function getSafeURL(url: string): string {
204
+ export function getSafeURL(url?: string): string {
205
+ url = url ? url : location.href;
146
206
  return encodeURI(decodeSafeURL(url));
147
207
  }
148
208
 
@@ -183,6 +243,13 @@ export function toBinaryStr(str: string) {
183
243
  return String.fromCharCode(...charCodes);
184
244
  }
185
245
 
246
+ /**
247
+ * Find a DOM element where an ad unit should be rendered to
248
+ */
249
+ export function getHtmlElement(id: string) {
250
+ return document.getElementById(id);
251
+ }
252
+
186
253
  /**
187
254
  * Convert an HTML string into a list of nodes
188
255
  */
@@ -216,7 +283,7 @@ export function createHtmlElement(
216
283
  */
217
284
  export function insertHtmlElements(
218
285
  nodes: NodeListOf<Node>,
219
- appendTo: Element | null = null,
286
+ appendTo?: Element,
220
287
  ) {
221
288
  appendTo = appendTo || document.body;
222
289
 
@@ -242,7 +309,11 @@ export function insertHtmlElements(
242
309
  flatHtmlAttributes(node.attributes),
243
310
  );
244
311
  } else {
245
- newNode = createScriptElement(node.innerHTML, true);
312
+ newNode = createScriptElement(
313
+ node.innerHTML,
314
+ true,
315
+ flatHtmlAttributes(node.attributes),
316
+ );
246
317
  }
247
318
  break;
248
319
 
@@ -340,6 +411,18 @@ export function createStyleElement(css: string | null) {
340
411
  return element;
341
412
  }
342
413
 
414
+ /**
415
+ * Create a <link> element
416
+ */
417
+ export function createLinkElement(href: string) {
418
+ const element = createHtmlElement("link") as HTMLLinkElement;
419
+
420
+ element.rel = "stylesheet";
421
+ element.href = href;
422
+
423
+ return element;
424
+ }
425
+
343
426
  /**
344
427
  * Create svg elements
345
428
  */
@@ -360,3 +443,471 @@ export function createSvgElement(
360
443
 
361
444
  return element;
362
445
  }
446
+
447
+ /**
448
+ * Create an image element to use as a tracking pixel
449
+ */
450
+ export function createPixelElement(src: string) {
451
+ const image = document.createElement("img");
452
+
453
+ image.src = src;
454
+ image.width = 1;
455
+ image.height = 1;
456
+ image.style.position = "absolute";
457
+ image.style.left = "-99999px";
458
+
459
+ return image;
460
+ }
461
+
462
+ /**
463
+ * Append a new script element to the DOM head
464
+ */
465
+ export function insertJs(js: string, inline = false) {
466
+ const element = createScriptElement(js, inline);
467
+
468
+ document.head.appendChild(element);
469
+ }
470
+
471
+ /**
472
+ * Append a new style element to the DOM head
473
+ */
474
+ export function insertCss(css: string, className?: string, external = false) {
475
+ const element = external ? createLinkElement(css) : createStyleElement(css);
476
+
477
+ if (className) {
478
+ element.className = className;
479
+ }
480
+
481
+ document.head.appendChild(element);
482
+ }
483
+
484
+ /**
485
+ * Get screen sizes
486
+ */
487
+ export function getScreenSize(): { width: number; height: number } {
488
+ let width, height;
489
+
490
+ if (window.screen) {
491
+ width = screen.width;
492
+ height = screen.height;
493
+ } else {
494
+ width = window.innerWidth;
495
+ height = window.innerHeight;
496
+ }
497
+
498
+ return {
499
+ width: width,
500
+ height: height,
501
+ };
502
+ }
503
+
504
+ /**
505
+ * Get a random integer between min and max
506
+ */
507
+ export function getRandomInt(min = 0, max = 4294967295) {
508
+ min = Math.ceil(min);
509
+ max = Math.floor(max);
510
+
511
+ return Math.floor(Math.random() * (max - min)) + min;
512
+ }
513
+
514
+ /**
515
+ * Implementation of Java's String.hashCode() method
516
+ */
517
+ export function hashCode(str: string) {
518
+ let hash = 0,
519
+ chr;
520
+
521
+ for (let i = 0; i < str.length; i++) {
522
+ chr = str.charCodeAt(i);
523
+ hash = (hash << 5) - hash + chr;
524
+ hash |= 0; // convert to 32bit integer
525
+ }
526
+
527
+ return hash >>> 0;
528
+ }
529
+
530
+ /**
531
+ * Executes a callback as soon as DOM is interactive
532
+ */
533
+ export function onDOMReady(callback: Function, ...args: any[]) {
534
+ if (
535
+ document.readyState === "interactive" ||
536
+ document.readyState === "complete"
537
+ ) {
538
+ callback(...args);
539
+ } else {
540
+ document.addEventListener("DOMContentLoaded", () => callback(...args));
541
+ }
542
+ }
543
+
544
+ /**
545
+ * Calculate the percentage of two values
546
+ *
547
+ * @param a
548
+ * @param b
549
+ * @return number
550
+ */
551
+ export function toPercent(a: number, b: number): number {
552
+ return b > 0 ? (a / b) * 100 : 0;
553
+ }
554
+
555
+ /**
556
+ * Make a camelCase string
557
+ *
558
+ * @param string
559
+ * @return string
560
+ */
561
+ export function toCamelCase(string: string): string {
562
+ return string
563
+ .split(new RegExp(/[-_.]/))
564
+ .reduce(
565
+ (a: string, b: string) => a + b.charAt(0).toUpperCase() + b.slice(1),
566
+ );
567
+ }
568
+
569
+ /**
570
+ * Make a snake_case string
571
+ *
572
+ * @param string
573
+ * @return string
574
+ */
575
+ export function toSnakeCase(string: string): string {
576
+ return toCamelCase(string)
577
+ .replace(/(^[A-Z])?([A-Z])/gm, "$1_$2")
578
+ .toLowerCase();
579
+ }
580
+
581
+ /**
582
+ * Transform the first letter of the given string to the upper case
583
+ *
584
+ * @param string
585
+ * @return string
586
+ */
587
+ export function capitalize(string: string): string {
588
+ return string.charAt(0).toUpperCase() + string.slice(1);
589
+ }
590
+
591
+ /**
592
+ * Registry independent string sorting
593
+ *
594
+ * @param first
595
+ * @param second
596
+ */
597
+ export function sortByAlphabet(first: string, second: string): number {
598
+ return first.toLowerCase() > second.toLowerCase() ? 1 : -1;
599
+ }
600
+
601
+ /**
602
+ * @param length
603
+ * @return string of max length 100
604
+ */
605
+ export function getRandomStr(length?: number): string {
606
+ length = length ? (length > 100 ? 100 : length) : 10;
607
+
608
+ let str = Math.random().toString(36).substring(2);
609
+
610
+ while (str.length < length) {
611
+ str += Math.random().toString(36).substring(2);
612
+ }
613
+
614
+ const result = str.slice(-length);
615
+ return isNaN(Number(result)) ? result : getRandomStr(length);
616
+ }
617
+
618
+ export function getRandomItem(items: any[]): any {
619
+ return items[Math.floor(Math.random() * items.length)];
620
+ }
621
+
622
+ /**
623
+ * Summarize elements of the given array
624
+ *
625
+ * @param values
626
+ * @return number | string - a summary of all the numbers in the array or an empty string if
627
+ * there was only NaNs
628
+ */
629
+ export function sumArray(values: Array<any>): number | string {
630
+ if (values.every((value: any) => isNaN(value))) {
631
+ return "";
632
+ } else {
633
+ return values.reduce((sum: any, curr: any) => {
634
+ const value = Number(curr);
635
+
636
+ if (!isNaN(value)) {
637
+ return sum + value;
638
+ } else {
639
+ return sum;
640
+ }
641
+ }, 0);
642
+ }
643
+ }
644
+
645
+ /**
646
+ * Returns an array of the object keys which values are not present in filter
647
+ *
648
+ * @param obj
649
+ * @param values
650
+ */
651
+ export function filterObjectKeysByValues(
652
+ obj: { [key: string]: any },
653
+ values: Array<string | number | boolean | null | undefined>,
654
+ ): string[] {
655
+ values = Array.isArray(values) ? values : Array(values);
656
+
657
+ const keys = [];
658
+
659
+ for (const key in obj) {
660
+ if (!values.includes(obj[key])) {
661
+ keys.push(key);
662
+ }
663
+ }
664
+
665
+ return keys;
666
+ }
667
+
668
+ export function getFromObject(object: object, key: string): any {
669
+ return key.split(".").reduce((a: any, b: any) => {
670
+ return a[b];
671
+ }, object);
672
+ }
673
+
674
+ export function fireBlurEvent(
675
+ element: string | Element | null,
676
+ delay: number = 0,
677
+ ): void {
678
+ fireEvent("blur", element, delay);
679
+ }
680
+
681
+ export function fireInputEvent(
682
+ element: string | Element | null,
683
+ delay: number = 0,
684
+ ): void {
685
+ fireEvent("input", element, delay);
686
+ }
687
+
688
+ export function fireEvent(
689
+ eventName: string,
690
+ element: string | Element | null,
691
+ delay: number = 0,
692
+ ): void {
693
+ setTimeout(() => {
694
+ if (isString(element)) {
695
+ element = document.querySelector(element);
696
+ }
697
+
698
+ if (!isNullOrUndef(element)) {
699
+ element.dispatchEvent(
700
+ new Event(eventName, {
701
+ bubbles: true,
702
+ }),
703
+ );
704
+ }
705
+ }, delay);
706
+ }
707
+
708
+ export function hasObjectChanged(
709
+ object: { [key: string]: any },
710
+ values: any[],
711
+ ): boolean {
712
+ let hasChanged = false;
713
+
714
+ for (const key in object) {
715
+ let oldVal = object[key];
716
+ let [, newVal] = values.find(([newKey]) => key === newKey);
717
+
718
+ if (isObject(oldVal)) {
719
+ if (!isObject(newVal)) {
720
+ hasChanged = true;
721
+ } else {
722
+ hasChanged = hasObjectChanged(oldVal, Object.entries(newVal));
723
+ }
724
+ } else if (Array.isArray(newVal) || Array.isArray(oldVal)) {
725
+ newVal = Array.isArray(newVal) ? newVal : [];
726
+ oldVal = Array.isArray(oldVal) ? oldVal : [];
727
+
728
+ hasChanged = arrayDiff(newVal, oldVal).length > 0;
729
+ } else {
730
+ hasChanged = areDiff(newVal, oldVal);
731
+ }
732
+
733
+ if (hasChanged) {
734
+ break;
735
+ }
736
+ }
737
+
738
+ return hasChanged;
739
+ }
740
+
741
+ export function roundBigNum(number: number): number {
742
+ const digitsNum = Math.trunc(number).toString().length;
743
+
744
+ let roundTo = 0;
745
+ switch (true) {
746
+ case digitsNum > 2 && digitsNum < 5:
747
+ roundTo = 100;
748
+ break;
749
+ case digitsNum >= 5:
750
+ roundTo = 10000;
751
+ break;
752
+ }
753
+
754
+ return roundTo ? Math.round(number / roundTo) * roundTo : number;
755
+ }
756
+
757
+ export function extractNumbers(value: any): string {
758
+ if (!isStr(value) && !isNum(value)) {
759
+ return "";
760
+ }
761
+ return value.toString().replace(/[^0-9]/g, "");
762
+ }
763
+
764
+ export function removeSpaces(value: any): string {
765
+ if (!isStr(value) && !isNum(value)) {
766
+ return "";
767
+ }
768
+ return value.toString().replace(/\s/g, "");
769
+ }
770
+
771
+ export function roundUp(num: number, precision: number): number {
772
+ const multiplier = Number("1".padEnd(precision + 1, "0"));
773
+ return Math.ceil(num * multiplier) / multiplier;
774
+ }
775
+
776
+ export function roundDown(num: number, precision: number): number {
777
+ const multiplier = Number("1".padEnd(precision + 1, "0"));
778
+ return Math.floor(num * multiplier) / multiplier;
779
+ }
780
+
781
+ export function areDiff(
782
+ val1?: Primitive | null,
783
+ val2?: Primitive | null,
784
+ strict?: boolean,
785
+ ): boolean {
786
+ if (strict) {
787
+ return val1 !== val2;
788
+ }
789
+
790
+ // at least one value is not empty
791
+ if (Boolean(val1) || Boolean(val2)) {
792
+ return val1 != val2;
793
+ }
794
+
795
+ // both empty, but not equally empty!
796
+ // each one may be one of: empty string, null or undefined
797
+ return false;
798
+ }
799
+
800
+ export function flattenObject(obj: FlattenableObject, prefix = "") {
801
+ return Object.keys(obj).reduce((acc, k) => {
802
+ const pre = prefix.length ? prefix + "." : "";
803
+ if (isObject(obj[k]) || Array.isArray(obj[k])) {
804
+ Object.assign(acc, flattenObject(obj[k], pre + k));
805
+ } else {
806
+ acc[pre + k] = obj[k];
807
+ }
808
+ return acc;
809
+ }, {} as FlattenableObject);
810
+ }
811
+
812
+ export function flattenObjectAsArray(obj: FlattenableObject) {
813
+ const result: FlattenableObject = {};
814
+ const flat = flattenObject(obj);
815
+ Object.keys(flat).forEach((key) => {
816
+ const newKey = key.replaceAll(/\.([^.]+)/g, "[$1]");
817
+ result[newKey] = flat[key];
818
+ });
819
+
820
+ return result;
821
+ }
822
+
823
+ export function parseObjectPathStr(pathStr: string): string[] {
824
+ const pathParts = pathStr.replace(/\[/g, ".").replace(/\]/g, "").split(".");
825
+
826
+ if (pathParts.length > 1 && pathParts[1].includes("[")) {
827
+ return [pathParts[0], ...parseObjectPathStr(pathParts[1])];
828
+ }
829
+
830
+ return pathParts;
831
+ }
832
+
833
+ export function formatNumber(
834
+ num: number | string,
835
+ fractionDigits?: number,
836
+ ): string {
837
+ fractionDigits = isNullOrUndef(fractionDigits) ? 2 : fractionDigits;
838
+ return new Intl.NumberFormat("ru-RU", {
839
+ style: "decimal",
840
+ minimumFractionDigits: fractionDigits,
841
+ maximumFractionDigits: fractionDigits,
842
+ })
843
+ .format(Number(num))
844
+ .replace(",", ".")
845
+ .replace(/\u00A0/g, " "); // charCode 160, White-space
846
+ }
847
+
848
+ export function formatNumberWithSign(
849
+ num: number | string,
850
+ fractionDigits?: number,
851
+ ): string {
852
+ return formatWithSign(num, formatNumber(num, fractionDigits));
853
+ }
854
+
855
+ export function formatPercent(num: number | string): string {
856
+ return formatNumber(num) + "%";
857
+ }
858
+
859
+ export function formatWithSign(
860
+ num: number | string,
861
+ numString?: string,
862
+ ): string {
863
+ numString = numString ? numString : num.toString();
864
+ return (Number(num) < 0 ? "" : "+") + numString;
865
+ }
866
+
867
+ export function autoSizeText(
868
+ el: HTMLElement,
869
+ height: number,
870
+ minFontSize: number,
871
+ maxFontSize: number = 50,
872
+ ) {
873
+ let attempts = 30;
874
+
875
+ const resizeText = () => {
876
+ if (getTextHeight() === 0) {
877
+ return;
878
+ }
879
+ while (attempts && getTextHeight() > height) {
880
+ attempts--;
881
+ reduceText();
882
+ }
883
+ while (attempts && getTextHeight() < height) {
884
+ attempts--;
885
+ enlargeText();
886
+ }
887
+ };
888
+
889
+ const reduceText = () => {
890
+ const fontSize = getNumericStyleProp("fontSize", el);
891
+ const newFontSize = fontSize - 1;
892
+ if (fontSize > 1 && newFontSize >= minFontSize) {
893
+ el.style.fontSize = `${newFontSize}px`;
894
+ }
895
+ };
896
+
897
+ const enlargeText = () => {
898
+ const fontSize = getNumericStyleProp("fontSize", el);
899
+ const newFontSize = fontSize + 1;
900
+ if (newFontSize <= maxFontSize) {
901
+ el.style.fontSize = `${newFontSize}px`;
902
+ }
903
+ };
904
+
905
+ const getTextHeight = () =>
906
+ Math.floor(
907
+ el.scrollHeight -
908
+ getNumericStyleProp("paddingTop", el) -
909
+ getNumericStyleProp("paddingBottom", el),
910
+ );
911
+
912
+ resizeText();
913
+ }
package/src/types.d.ts CHANGED
@@ -1 +1,7 @@
1
- export type Primitive = string | boolean | number;
1
+ export type FlattenableObject = { [key: string]: any };
2
+
3
+ export type Primitive = boolean | number | string;
4
+
5
+ export type SimpleObject = {
6
+ [key: string | number]: Primitive | Primitive[] | null | undefined;
7
+ };