Repository: exadel-inc/esl
Pull request #259 (2021-06-16 23:40:49)

esl-utils: a batch of small updates and fixes


Patch:
diff --git a/src/modules/esl-media-query/core/esl-media-rule-list.ts b/src/modules/esl-media-query/core/esl-media-rule-list.ts
index c0ef6e05c..941812b16 100644
--- a/src/modules/esl-media-query/core/esl-media-rule-list.ts
+++ b/src/modules/esl-media-query/core/esl-media-rule-list.ts
@@ -1,7 +1,7 @@
 import {Observable} from '../../esl-utils/abstract/observable';
+import {isPrimitive} from '../../esl-utils/misc/object';
 import {evaluate} from '../../esl-utils/misc/format';
 import {ESLMediaRule} from './esl-media-rule';
-import {isPrimitive} from '../../esl-utils/misc/object';
 
 type PayloadParser<T> = (val: string) => T | undefined;
 
diff --git a/src/modules/esl-toggleable/core/esl-toggleable.ts b/src/modules/esl-toggleable/core/esl-toggleable.ts
index b7b9f7920..40ad76b79 100644
--- a/src/modules/esl-toggleable/core/esl-toggleable.ts
+++ b/src/modules/esl-toggleable/core/esl-toggleable.ts
@@ -67,6 +67,9 @@ export class ESLToggleable extends ESLBaseElement {
   /** Default params to merge into passed action params */
   @jsonAttr<ToggleableActionParams>({defaultValue: {}})
   public defaultParams: ToggleableActionParams;
+  /** Hover params to pass from track hover listener */
+  @jsonAttr<ToggleableActionParams>({defaultValue: {}})
+  public trackHoverParams: ToggleableActionParams;
 
   /** Marker of initially opened toggleable instance */
   public initiallyOpened: boolean;
@@ -273,10 +276,12 @@ export class ESLToggleable extends ESLBaseElement {
 
   @bind
   protected _onMouseEnter(e: MouseEvent) {
-    this.show({initiator: 'mouseenter', trackHover: true, activator: this, event: e});
+    const baseParams = {initiator: 'mouseenter', trackHover: true, activator: this, event: e};
+    this.show(Object.assign(baseParams, this.trackHoverParams));
   }
   @bind
   protected _onMouseLeave(e: MouseEvent) {
-    this.hide({initiator: 'mouseleave', trackHover: true, activator: this, event: e});
+    const baseParams = {initiator: 'mouseleave', trackHover: true, activator: this, event: e};
+    this.hide(Object.assign(baseParams, this.trackHoverParams));
   }
 }
diff --git a/src/modules/esl-trigger/core/esl-trigger.ts b/src/modules/esl-trigger/core/esl-trigger.ts
index c6ed1a5ec..d4f221d44 100644
--- a/src/modules/esl-trigger/core/esl-trigger.ts
+++ b/src/modules/esl-trigger/core/esl-trigger.ts
@@ -2,6 +2,7 @@ import {ExportNs} from '../../esl-utils/environment/export-ns';
 import {attr, boolAttr, ESLBaseElement} from '../../esl-base-element/core';
 import {bind} from '../../esl-utils/decorators/bind';
 import {ready} from '../../esl-utils/decorators/ready';
+import {parseNumber} from '../../esl-utils/misc/format';
 import {CSSClassUtils} from '../../esl-utils/dom/class';
 import {ENTER, SPACE} from '../../esl-utils/dom/keys';
 import {TraversingQuery} from '../../esl-traversing-query/core';
@@ -85,10 +86,6 @@ export class ESLTrigger extends ESLBaseElement {
     return ESLMediaQuery.for(this.trackClick).matches;
   }
 
-  private static parseDelayValue(delay: string): number | undefined {
-    return isNaN(+delay) ? undefined : +delay;
-  }
-
   @ready
   protected connectedCallback() {
     super.connectedCallback();
@@ -146,14 +143,14 @@ export class ESLTrigger extends ESLBaseElement {
   /** Show target toggleable with passed params */
   public showTarget(params: ToggleableActionParams = {}) {
     const actionParams = this.mergeToggleableParams({
-      delay: ESLTrigger.parseDelayValue(this.showDelay)
+      delay: parseNumber(this.showDelay)
     }, params);
     this.$target && this.$target.show(actionParams);
   }
   /** Hide target toggleable with passed params */
   public hideTarget(params: ToggleableActionParams = {}) {
     const actionParams = this.mergeToggleableParams({
-      delay: ESLTrigger.parseDelayValue(this.hideDelay)
+      delay: parseNumber(this.hideDelay)
     }, params);
     this.$target && this.$target.hide(actionParams);
   }
@@ -209,7 +206,7 @@ export class ESLTrigger extends ESLBaseElement {
   @bind
   protected _onMouseEnter(event: MouseEvent) {
     if (!this.allowHover) return;
-    const delay = ESLTrigger.parseDelayValue(this.hoverShowDelay);
+    const delay = parseNumber(this.hoverShowDelay);
     this.toggleTarget({event, delay}, this.mode !== 'hide');
     event.preventDefault();
   }
@@ -219,7 +216,7 @@ export class ESLTrigger extends ESLBaseElement {
   protected _onMouseLeave(event: MouseEvent) {
     if (!this.allowHover) return;
     if (this.mode === 'show' || this.mode === 'hide') return;
-    const delay = ESLTrigger.parseDelayValue(this.hoverHideDelay);
+    const delay = parseNumber(this.hoverHideDelay);
     this.hideTarget({event, delay, trackHover: true});
     event.preventDefault();
   }
diff --git a/src/modules/esl-utils/README.md b/src/modules/esl-utils/README.md
index 265573aca..63695540a 100644
--- a/src/modules/esl-utils/README.md
+++ b/src/modules/esl-utils/README.md
@@ -18,7 +18,7 @@ Even if you use the whole utils module, it is still tiny.
   
   - #### [Aggregate](./async/aggregate.ts) - throttle function decorator with aggregation.
     Limits decorated function calls to one call in passed `time`. The decorated function `callback` will be called once 
-    at the end of the timeout with the list of first arguments in the calls.
+    at the end of the timeout with the list of the first arguments in the calls.
 
   - #### [Debounce](./async/debounce.ts) - debounce function decorator.
     Debouncing is a pattern commonly used for rate limiting function calls with a timeout.
diff --git a/src/modules/esl-utils/async/aggregate.ts b/src/modules/esl-utils/async/aggregate.ts
index f80d9d511..2ae3489eb 100644
--- a/src/modules/esl-utils/async/aggregate.ts
+++ b/src/modules/esl-utils/async/aggregate.ts
@@ -1,19 +1,19 @@
 /**
  * Aggregate is a function decorator that limits decorated function calls to one call in passed `time`.
- * The decorated function `callback` will be called once at the end of the timeout with the list of first arguments in the calls.
+ * The decorated function `callback` will be called once at the end of the timeout with the list of the first arguments in the calls.
  */
 export function aggregate<T>(callback: (this: void, args: T[]) => void, time: number) {
-  let calls: T[] = [];
+  let params: T[] = [];
   let timeout = 0;
 
   const emit = () => {
-    callback(calls);
-    calls = [];
+    callback(params);
+    params = [];
     timeout = 0;
   };
 
   return function (arg: T) {
-    calls.push(arg);
+    params.push(arg);
     timeout = timeout || window.setTimeout(emit, time);
   };
 }
diff --git a/src/modules/esl-utils/dom/scroll.less b/src/modules/esl-utils/dom/scroll.less
index 47fba1031..035da7bd0 100644
--- a/src/modules/esl-utils/dom/scroll.less
+++ b/src/modules/esl-utils/dom/scroll.less
@@ -20,8 +20,12 @@ html.esl-disable-scroll {
     display: flex !important;
     align-items: stretch !important;
 
+    &[dir='rtl'] {
+      flex-flow: row-reverse !important;
+    }
+
     body {
-      flex: 1 1 auto !important;
+      flex: 1 1 100% !important;
       max-height: 100vh !important;
     }
 
diff --git a/src/modules/esl-utils/misc/format.ts b/src/modules/esl-utils/misc/format.ts
index 3c1a2402f..f4ebbae6b 100644
--- a/src/modules/esl-utils/misc/format.ts
+++ b/src/modules/esl-utils/misc/format.ts
@@ -15,6 +15,16 @@ export const unwrapParenthesis = (str: string) => {
   return str.trim().replace(/^\((.*)\)$/, '$1').trim();
 };
 
+/**
+ * Parse number with the ability to pass an alternative fallback for NaN.
+ * Note: falsy values except 0 are treated as NaN
+ */
+export const parseNumber = (str: string | number, nanValue?: number | undefined) => {
+  if (str === 0) return 0;
+  const value = +(str || NaN);
+  return isNaN(value) ? nanValue : value;
+};
+
 /**
  * Common function that returns coefficient aspect ratio
  * Supported formats: w:h, w/h, coefficient
diff --git a/src/modules/esl-utils/misc/uid.ts b/src/modules/esl-utils/misc/uid.ts
index ab93b210e..0f8fadc71 100644
--- a/src/modules/esl-utils/misc/uid.ts
+++ b/src/modules/esl-utils/misc/uid.ts
@@ -1,4 +1,12 @@
-const sequences = new Map<string, number>();
+const ns = window || global;
+const sequences = ns.__esl_sequences__ || new Map<string, number>();
+ns.__esl_sequences__ = sequences;
+
+declare global {
+  interface Window {
+    __esl_sequences__: Map<string, number>;
+  }
+}
 
 /** Create and return sequential id */
 export const sequentialUID = (name: string, prefix: string = name) => {
@@ -7,20 +15,11 @@ export const sequentialUID = (name: string, prefix: string = name) => {
   return prefix + uid;
 };
 
-/** Reset {@link sequentialUID} generator */
-export const resetSequentialUID = (name?: string) => {
-  if (typeof name === 'string') {
-    sequences.delete(name);
-  } else {
-    sequences.clear();
-  }
-};
-
 /** Return random unique identifier */
-export const randUID = (): string => {
+export const randUID = (prefix: string = ''): string => {
   const time = Date.now().toString(32);
   const rand = Math.round(Math.random() * 1024 * 1024).toString(32);
-  return time + '-' + rand;
+  return prefix + time + '-' + rand;
 };
 
 /**


Test patch:
diff --git a/src/modules/esl-utils/misc/test/format.test.ts b/src/modules/esl-utils/misc/test/format.test.ts
index 83337a8ad..b53339a9c 100644
--- a/src/modules/esl-utils/misc/test/format.test.ts
+++ b/src/modules/esl-utils/misc/test/format.test.ts
@@ -1,4 +1,4 @@
-import {toCamelCase, toKebabCase, unwrapParenthesis, parseAspectRatio, evaluate, format} from '../format';
+import {toCamelCase, toKebabCase, unwrapParenthesis, parseAspectRatio, evaluate, format, parseNumber} from '../format';
 
 describe('misc/format helper tests', () => {
   test('toKebabCase', () => {
@@ -72,4 +72,21 @@ describe('misc/format helper tests', () => {
       expect(format(tmp, source)).toBe(res);
     });
   });
+
+  describe('parseNumber', () => {
+    test.each([
+      [['123'], 123],
+      [['123', 1], 123],
+      [[0], 0],
+      [['a'], undefined],
+      [['b', undefined], undefined],
+      [['abc', NaN], NaN],
+      [['def', 0], 0],
+      [[''], undefined],
+      [[false], undefined]
+    ])(
+      'args = %p, result: %p',
+      (args, exp) => expect(parseNumber.apply(null, args)).toBe(exp)
+    );
+  });
 });
diff --git a/src/modules/esl-utils/misc/test/uid.test.ts b/src/modules/esl-utils/misc/test/uid.test.ts
index d07693865..623da53a3 100644
--- a/src/modules/esl-utils/misc/test/uid.test.ts
+++ b/src/modules/esl-utils/misc/test/uid.test.ts
@@ -1,4 +1,4 @@
-import {randUID, resetSequentialUID, sequentialUID} from '../uid';
+import {randUID, sequentialUID} from '../uid';
 
 const TRY_COUNT = 5;
 
@@ -21,17 +21,4 @@ describe('misc/uid helper tests', () => {
     }
     expect(set.size).toBe(TRY_COUNT * 2);
   });
-
-  test('reset seqUID', () => {
-    const set = new Set();
-    for (let i = 0; i < TRY_COUNT; ++i) {
-      set.add(sequentialUID('c'));
-    }
-    expect(set.size).toBe(TRY_COUNT);
-    resetSequentialUID('c');
-    for (let i = 0; i < TRY_COUNT; ++i) {
-      set.add(sequentialUID('c'));
-    }
-    expect(set.size).toBe(TRY_COUNT);
-  });
 });