From 9c78249385fe6c9a5c620cbe5031d3419384f64e Mon Sep 17 00:00:00 2001 From: "ala'n (Alexey Stsefanovich)" Date: Mon, 30 Oct 2023 17:10:58 +0100 Subject: [PATCH 1/3] feat(esl-popup): internal ESLPopup implementation updated to use ESLEventListeners; usage of cached properties reduced --- src/modules/esl-popup/core/esl-popup.ts | 234 ++++++++++-------------- 1 file changed, 99 insertions(+), 135 deletions(-) diff --git a/src/modules/esl-popup/core/esl-popup.ts b/src/modules/esl-popup/core/esl-popup.ts index a923457b9..1e8608e60 100644 --- a/src/modules/esl-popup/core/esl-popup.ts +++ b/src/modules/esl-popup/core/esl-popup.ts @@ -1,6 +1,6 @@ import {range} from '../../esl-utils/misc/array'; import {ExportNs} from '../../esl-utils/environment/export-ns'; -import {bind, memoize, ready, prop, attr, boolAttr, jsonAttr} from '../../esl-utils/decorators'; +import {bind, memoize, ready, attr, boolAttr, jsonAttr, listen, decorate} from '../../esl-utils/decorators'; import {ESLTraversingQuery} from '../../esl-traversing-query/core'; import {afterNextRender, rafDecorator} from '../../esl-utils/async/raf'; import {ESLToggleable} from '../../esl-toggleable/core'; @@ -8,16 +8,18 @@ import {Rect} from '../../esl-utils/dom/rect'; import {isRTL} from '../../esl-utils/dom/rtl'; import {getListScrollParents} from '../../esl-utils/dom/scroll'; import {getWindowRect} from '../../esl-utils/dom/window'; -import {parseNumber} from '../../esl-utils/misc/format'; +import {parseBoolean, parseNumber, toBooleanAttribute} from '../../esl-utils/misc/format'; +import {copy} from '../../esl-utils/misc/object'; +import {ESLIntersectionTarget, ESLIntersectionEvent} from '../../esl-event-listener/core/targets/intersection.target'; import {calcPopupPosition, isMajorAxisHorizontal} from './esl-popup-position'; import {ESLPopupPlaceholder} from './esl-popup-placeholder'; import type {ESLToggleableActionParams} from '../../esl-toggleable/core'; import type {PositionType, IntersectionRatioRect} from './esl-popup-position'; + const INTERSECTION_LIMIT_FOR_ADJACENT_AXIS = 0.7; const DEFAULT_OFFSET_ARROW = 50; -const scrollOptions = {passive: true} as EventListenerOptions; const parsePercent = (value: string | number, nanValue: number = 0): number => { const rawValue = parseNumber(value, nanValue); @@ -51,26 +53,10 @@ export interface PopupActionParams extends ESLToggleableActionParams { containerEl?: HTMLElement; } -export interface ActivatorObserver { - unsubscribers?: (() => void)[]; - observer?: IntersectionObserver; -} - @ExportNs('Popup') export class ESLPopup extends ESLToggleable { public static override is = 'esl-popup'; - public $placeholder: ESLPopupPlaceholder | null; - - protected _containerEl?: HTMLElement; - protected _offsetTrigger: number; - protected _offsetContainer: number | [number, number]; - protected _deferredUpdatePosition = rafDecorator(() => this._updatePosition()); - protected _activatorObserver: ActivatorObserver; - protected _intersectionMargin: string; - protected _intersectionRatio: IntersectionRatioRect = {}; - protected _updateLoopID: number; - /** Classname of popups arrow element */ @attr({defaultValue: 'esl-popup-arrow'}) public arrowClass: string; @@ -85,6 +71,7 @@ export class ESLPopup extends ESLToggleable { /** Disable hiding the popup depending on the visibility of the activator */ @boolAttr() public disableActivatorObservation: boolean; + /** * Margins on the edges of the arrow. * This is the value in pixels that will be between the edge of the popup and @@ -110,8 +97,19 @@ export class ESLPopup extends ESLToggleable { }}) public override defaultParams: PopupActionParams; - @prop() public override closeOnEsc = true; - @prop() public override closeOnOutsideAction = true; + @attr({parser: parseBoolean, serializer: toBooleanAttribute, defaultValue: true}) + public override closeOnEsc: boolean; + @attr({parser: parseBoolean, serializer: toBooleanAttribute, defaultValue: true}) + public override closeOnOutsideAction: boolean; + + public $placeholder: ESLPopupPlaceholder | null; + + protected _containerEl?: HTMLElement; + protected _offsetTrigger: number; + protected _offsetContainer: number | [number, number]; + protected _intersectionMargin: string; + protected _intersectionRatio: IntersectionRatioRect = {}; + protected _updateLoopID: number; /** Arrow element */ @memoize() @@ -144,18 +142,6 @@ export class ESLPopup extends ESLToggleable { memoize.clear(this, '$arrow'); } - /** Checks that the position along the horizontal axis */ - @memoize() - protected get _isMajorAxisHorizontal(): boolean { - return isMajorAxisHorizontal(this.position); - } - - /** Checks that the position along the vertical axis */ - @memoize() - protected get _isMajorAxisVertical(): boolean { - return !isMajorAxisHorizontal(this.position); - } - /** Get offsets arrow ratio */ @memoize() protected get _offsetArrowRatio(): number { @@ -186,15 +172,16 @@ export class ESLPopup extends ESLToggleable { return $arrow; } - /** - * Actions to execute before showing of popup. Handles the activator and updates the position of the popup. - * @returns false if the show task should be canceled - */ + /** Runs additional actions on show popup request */ protected override onBeforeShow(params: ESLToggleableActionParams): boolean | void { - if (this.open) this.afterOnHide(); this.activator = params.activator; - if (this.open) this.afterOnShow(); - if (this.open && !params.force) return false; // the show task will be forced to run so the next steps are unnecessary + if (this.open) { + this.afterOnHide(); + this.afterOnShow(); + } + + if (!params.force && this.open) return false; + if (!params.silent && !this.$$fire(this.BEFORE_SHOW_EVENT, {detail: {params}})) return false; } /** @@ -205,24 +192,16 @@ export class ESLPopup extends ESLToggleable { protected override onShow(params: PopupActionParams): void { super.onShow(params); - if (params.position) { - this.position = params.position; - } - if (params.behavior) { - this.behavior = params.behavior; - } - if (params.disableActivatorObservation) { - this.disableActivatorObservation = params.disableActivatorObservation; - } - if (params.marginArrow) { - this.marginArrow = params.marginArrow; - } - if (params.offsetArrow) { - this.offsetArrow = params.offsetArrow; - } - if (params.container) { - this.container = params.container; - } + // TODO: change flow to use merged params unless attribute state is used in CSS + Object.assign(this, copy({ + position: params.position, + behavior: params.behavior, + container: params.container, + marginArrow: params.marginArrow, + offsetArrow: params.offsetArrow, + disableActivatorObservation: params.disableActivatorObservation + }, (key, val): boolean => !!val)); + this._containerEl = params.containerEl; this._offsetTrigger = params.offsetTrigger || 0; this._offsetContainer = params.offsetContainer || 0; @@ -250,7 +229,12 @@ export class ESLPopup extends ESLToggleable { protected afterOnShow(): void { this._updatePosition(); this.style.visibility = 'visible'; - this.activator && this._addActivatorObserver(this.activator); + + this.$$on(this._onActivatorScroll); + this.$$on(this._onActivatorIntersection); + this.$$on(this._onTransitionStart); + this.$$on(this._onResize); + this._startUpdateLoop(); } @@ -264,97 +248,81 @@ export class ESLPopup extends ESLToggleable { */ protected afterOnHide(): void { this._stopUpdateLoop(); - this.activator && this._removeActivatorObserver(this.activator); - memoize.clear(this, ['_isMajorAxisHorizontal', '_isMajorAxisVertical', '_offsetArrowRatio', '$container']); + this.$$off(this._onActivatorScroll); + this.$$off(this._onActivatorIntersection); + this.$$off(this._onTransitionStart); + this.$$off(this._onResize); + + memoize.clear(this, ['_offsetArrowRatio', '$container']); } - /** - * Checks activator intersection for adjacent axis. - * Hides the popup if the intersection ratio exceeds the limit. - */ - protected _checkIntersectionForAdjacentAxis(isAdjacentAxis: boolean, intersectionRatio: number): void { - if (isAdjacentAxis && intersectionRatio < INTERSECTION_LIMIT_FOR_ADJACENT_AXIS) { - this.hide(); + protected get scrollTargets(): EventTarget[] { + if (this.activator) { + return (getListScrollParents(this.activator) as EventTarget[]).concat([window]); } + return [window]; + } + + protected get intersectionOptions(): IntersectionObserverInit { + return { + rootMargin: this._intersectionMargin, + threshold: range(9, (x) => x / 8) + }; } /** Actions to execute on activator intersection event. */ - @bind - protected onActivatorIntersection(entries: IntersectionObserverEntry[], observer: IntersectionObserver): void { - const entry = entries[0]; + @listen({ + auto: false, + event: ESLIntersectionEvent.type, + target: ($popup: ESLPopup) => $popup.activator ? ESLIntersectionTarget.for($popup.activator, $popup.intersectionOptions) : [], + condition: ($popup: ESLPopup) => !$popup.disableActivatorObservation + }) + protected _onActivatorIntersection(event: ESLIntersectionEvent): void { this._intersectionRatio = {}; - if (!entry.isIntersecting) { + if (!event.isIntersecting) { this.hide(); return; } - if (entry.intersectionRect.y !== entry.boundingClientRect.y) { - this._intersectionRatio.top = entry.intersectionRect.height / entry.boundingClientRect.height; - this._checkIntersectionForAdjacentAxis(this._isMajorAxisHorizontal, this._intersectionRatio.top); + const isHorizontal = isMajorAxisHorizontal(this.position); + const checkIntersection = (isMajorAxis: boolean, intersectionRatio: number): void => { + if (isMajorAxis && intersectionRatio < INTERSECTION_LIMIT_FOR_ADJACENT_AXIS) this.hide(); + }; + if (event.intersectionRect.y !== event.boundingClientRect.y) { + this._intersectionRatio.top = event.intersectionRect.height / event.boundingClientRect.height; + checkIntersection(isHorizontal, this._intersectionRatio.top); } - if (entry.intersectionRect.bottom !== entry.boundingClientRect.bottom) { - this._intersectionRatio.bottom = entry.intersectionRect.height / entry.boundingClientRect.height; - this._checkIntersectionForAdjacentAxis(this._isMajorAxisHorizontal, this._intersectionRatio.bottom); + if (event.intersectionRect.bottom !== event.boundingClientRect.bottom) { + this._intersectionRatio.bottom = event.intersectionRect.height / event.boundingClientRect.height; + checkIntersection(isHorizontal, this._intersectionRatio.bottom); } - if (entry.intersectionRect.x !== entry.boundingClientRect.x) { - this._intersectionRatio.left = entry.intersectionRect.width / entry.boundingClientRect.width; - this._checkIntersectionForAdjacentAxis(this._isMajorAxisVertical, this._intersectionRatio.left); + if (event.intersectionRect.x !== event.boundingClientRect.x) { + this._intersectionRatio.left = event.intersectionRect.width / event.boundingClientRect.width; + checkIntersection(!isHorizontal, this._intersectionRatio.left); } - if (entry.intersectionRect.right !== entry.boundingClientRect.right) { - this._intersectionRatio.right = entry.intersectionRect.width / entry.boundingClientRect.width; - this._checkIntersectionForAdjacentAxis(this._isMajorAxisVertical, this._intersectionRatio.right); + if (event.intersectionRect.right !== event.boundingClientRect.right) { + this._intersectionRatio.right = event.intersectionRect.width / event.boundingClientRect.width; + checkIntersection(!isHorizontal, this._intersectionRatio.right); } } /** Actions to execute on activator scroll event. */ - @bind - protected onActivatorScroll(e: Event): void { + @listen({auto: false, event: 'scroll', target: ($popup: ESLPopup) => $popup.scrollTargets}) + protected _onActivatorScroll(e: Event): void { if (this._updateLoopID) return; this._updatePosition(); } - /** Creates listeners and observers to observe activator after showing popup */ - protected _addActivatorObserver(target: HTMLElement): void { - const scrollParents = getListScrollParents(target); - - this._activatorObserver = { - unsubscribers: scrollParents.map(($root) => { - $root.addEventListener('scroll', this.onActivatorScroll, scrollOptions); - return (): void => { - $root && $root.removeEventListener('scroll', this.onActivatorScroll, scrollOptions); - }; - }) - }; - - if (!this.disableActivatorObservation) { - const options = { - rootMargin: this._intersectionMargin, - threshold: range(9, (x) => x / 8) - } as IntersectionObserverInit; - - const observer = new IntersectionObserver(this.onActivatorIntersection, options); - observer.observe(target); - - this._activatorObserver.observer = observer; - } - - window.addEventListener('resize', this._deferredUpdatePosition); - window.addEventListener('scroll', this.onActivatorScroll, scrollOptions); - document.body.addEventListener('transitionstart', this._startUpdateLoop); + @listen({auto: false, event: 'transitionstart', target: document.body}) + protected _onTransitionStart(): void { + this._startUpdateLoop(); } - /** Removes activator listeners and observers after hiding popup */ - protected _removeActivatorObserver(target: HTMLElement): void { - window.removeEventListener('resize', this._deferredUpdatePosition); - window.removeEventListener('scroll', this.onActivatorScroll, scrollOptions); - document.body.removeEventListener('transitionstart', this._startUpdateLoop); - - if (!this._activatorObserver) return; - this._activatorObserver.observer?.disconnect(); - this._activatorObserver.observer = undefined; - this._activatorObserver.unsubscribers?.forEach((cb) => cb()); - this._activatorObserver.unsubscribers = []; + @listen({auto: false, event: 'resize', target: window}) + @decorate(rafDecorator) + protected _onResize(): void { + this._updatePosition(); } /** @@ -369,10 +337,7 @@ export class ESLPopup extends ESLToggleable { let same = 0; let lastRect = new Rect(); const updateLoop = (): void => { - if (!this.activator) { - this._stopUpdateLoop(); - return; - } + if (!this.activator) return this._stopUpdateLoop(); const newRect = Rect.from(this.activator.getBoundingClientRect()); if (!Rect.isEqual(lastRect, newRect)) { @@ -380,10 +345,8 @@ export class ESLPopup extends ESLToggleable { lastRect = newRect; } - if (same++ > 2) { - this._stopUpdateLoop(); - return; - } + if (same++ > 2) return this._stopUpdateLoop(); + this._updatePosition(); this._updateLoopID = requestAnimationFrame(updateLoop); }; @@ -436,8 +399,9 @@ export class ESLPopup extends ESLToggleable { this.style.top = `${popup.y}px`; // set arrow position if (this.$arrow) { - this.$arrow.style.left = this._isMajorAxisVertical ? `${arrow.x}px` : ''; - this.$arrow.style.top = this._isMajorAxisHorizontal ? `${arrow.y}px` : ''; + const isHorizontal = isMajorAxisHorizontal(this.position); + this.$arrow.style.left = isHorizontal ? '' : `${arrow.x}px`; + this.$arrow.style.top = isHorizontal ? `${arrow.y}px` : ''; } } } From 014ebe49aa7fc294be4240e60c22f3749794fa0f Mon Sep 17 00:00:00 2001 From: "ala'n (Alexey Stsefanovich)" Date: Mon, 30 Oct 2023 17:39:42 +0100 Subject: [PATCH 2/3] refactor(esl-popup): remove extra before event call from esl-popup implementation --- src/modules/esl-popup/core/esl-popup.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/esl-popup/core/esl-popup.ts b/src/modules/esl-popup/core/esl-popup.ts index 1e8608e60..fa8b0f295 100644 --- a/src/modules/esl-popup/core/esl-popup.ts +++ b/src/modules/esl-popup/core/esl-popup.ts @@ -181,7 +181,6 @@ export class ESLPopup extends ESLToggleable { } if (!params.force && this.open) return false; - if (!params.silent && !this.$$fire(this.BEFORE_SHOW_EVENT, {detail: {params}})) return false; } /** From 7c2169916b9cea5472fbbeb5aed9e02683a0459e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 11:23:09 +0000 Subject: [PATCH 3/3] chore(deps-dev): bump eslint from 8.52.0 to 8.53.0 Bumps [eslint](https://github.com/eslint/eslint) from 8.52.0 to 8.53.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v8.52.0...v8.53.0) --- updated-dependencies: - dependency-name: eslint dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 30 +++++++++++++++--------------- package.json | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9071c58bd..5e5ad7ff0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "concurrently": "^8.2.2", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", - "eslint": "^8.52.0", + "eslint": "^8.53.0", "eslint-plugin-editorconfig": "^4.0.3", "eslint-plugin-import": "^2.29.0", "eslint-plugin-sonarjs": "^0.23.0", @@ -1482,9 +1482,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", + "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -1505,9 +1505,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", - "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", + "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -6504,15 +6504,15 @@ } }, "node_modules/eslint": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", - "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", + "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.52.0", + "@eslint/eslintrc": "^2.1.3", + "@eslint/js": "8.53.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -7701,9 +7701,9 @@ } }, "node_modules/globals": { - "version": "13.21.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", - "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "dependencies": { "type-fest": "^0.20.2" diff --git a/package.json b/package.json index 746b9cce2..fc7ba607f 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "concurrently": "^8.2.2", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", - "eslint": "^8.52.0", + "eslint": "^8.53.0", "eslint-plugin-editorconfig": "^4.0.3", "eslint-plugin-import": "^2.29.0", "eslint-plugin-sonarjs": "^0.23.0",