Files
Brandy Smith d52fca084c fix(config): allow LogLevel to work with isolatedModules and update all warns and errors to respect logLevel (#30350)
Issue number: internal

---------

## What is the current behavior?
- `LogLevel` throws error `Error: Cannot access ambient const enums when
'isolatedModules' is enabled`
- Several existing console warns and errors are not calling the function
that respects the `logLevel` config

## What is the new behavior?
- Remove `const` from the `enum` to work with `isolatedModules`
- Update `console.warn`s to `printIonWarning`
- Update `console.error`s to `printIonError`

## Does this introduce a breaking change?
- [ ] Yes
- [x] No

## Other information

Dev build: `8.5.5-dev.11744729748.174bf7e0`

---------

Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
2025-04-16 16:23:16 +00:00

519 lines
15 KiB
TypeScript

import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Method, Prop, State, Watch, h } from '@stencil/core';
import { findClosestIonContent, disableContentScrollY, resetContentScrollY } from '@utils/content';
import { isEndSide } from '@utils/helpers';
import { printIonWarning } from '@utils/logging';
import { watchForOptions } from '@utils/watch-options';
import { getIonMode } from '../../global/ionic-global';
import type { Gesture, GestureDetail } from '../../interface';
import type { Side } from '../menu/menu-interface';
const SWIPE_MARGIN = 30;
const ELASTIC_FACTOR = 0.55;
const enum ItemSide {
None = 0,
Start = 1 << 0,
End = 1 << 1,
Both = Start | End,
}
const enum SlidingState {
Disabled = 1 << 1,
Enabled = 1 << 2,
End = 1 << 3,
Start = 1 << 4,
SwipeEnd = 1 << 5,
SwipeStart = 1 << 6,
}
let openSlidingItem: HTMLIonItemSlidingElement | undefined;
@Component({
tag: 'ion-item-sliding',
styleUrl: 'item-sliding.scss',
})
export class ItemSliding implements ComponentInterface {
private item: HTMLIonItemElement | null = null;
private openAmount = 0;
private initialOpenAmount = 0;
private optsWidthRightSide = 0;
private optsWidthLeftSide = 0;
private sides = ItemSide.None;
private tmr?: ReturnType<typeof setTimeout>;
private leftOptions?: HTMLIonItemOptionsElement;
private rightOptions?: HTMLIonItemOptionsElement;
private optsDirty = true;
private gesture?: Gesture;
private contentEl: HTMLElement | null = null;
private initialContentScrollY = true;
private mutationObserver?: MutationObserver;
@Element() el!: HTMLIonItemSlidingElement;
@State() state: SlidingState = SlidingState.Disabled;
/**
* If `true`, the user cannot interact with the sliding item.
*/
@Prop() disabled = false;
@Watch('disabled')
disabledChanged() {
if (this.gesture) {
this.gesture.enable(!this.disabled);
}
}
/**
* Emitted when the sliding position changes.
*/
@Event() ionDrag!: EventEmitter;
async connectedCallback() {
const { el } = this;
this.item = el.querySelector('ion-item');
this.contentEl = findClosestIonContent(el);
/**
* The MutationObserver needs to be added before we
* call updateOptions below otherwise we may miss
* ion-item-option elements that are added to the DOM
* while updateOptions is running and before the MutationObserver
* has been initialized.
*/
this.mutationObserver = watchForOptions<HTMLIonItemOptionElement>(el, 'ion-item-option', async () => {
await this.updateOptions();
});
await this.updateOptions();
this.gesture = (await import('../../utils/gesture')).createGesture({
el,
gestureName: 'item-swipe',
gesturePriority: 100,
threshold: 5,
canStart: (ev) => this.canStart(ev),
onStart: () => this.onStart(),
onMove: (ev) => this.onMove(ev),
onEnd: (ev) => this.onEnd(ev),
});
this.disabledChanged();
}
disconnectedCallback() {
if (this.gesture) {
this.gesture.destroy();
this.gesture = undefined;
}
this.item = null;
this.leftOptions = this.rightOptions = undefined;
if (openSlidingItem === this.el) {
openSlidingItem = undefined;
}
if (this.mutationObserver) {
this.mutationObserver.disconnect();
this.mutationObserver = undefined;
}
}
/**
* Get the amount the item is open in pixels.
*/
@Method()
getOpenAmount(): Promise<number> {
return Promise.resolve(this.openAmount);
}
/**
* Get the ratio of the open amount of the item compared to the width of the options.
* If the number returned is positive, then the options on the right side are open.
* If the number returned is negative, then the options on the left side are open.
* If the absolute value of the number is greater than 1, the item is open more than
* the width of the options.
*/
@Method()
getSlidingRatio(): Promise<number> {
return Promise.resolve(this.getSlidingRatioSync());
}
/**
* Open the sliding item.
*
* @param side The side of the options to open. If a side is not provided, it will open the first set of options it finds within the item.
*/
@Method()
async open(side: Side | undefined) {
/**
* It is possible for the item to be added to the DOM
* after the item-sliding component was created. As a result,
* if this.item is null, then we should attempt to
* query for the ion-item again.
* However, if the item is already defined then
* we do not query for it again.
*/
const item = (this.item = this.item ?? this.el.querySelector('ion-item'));
if (item === null) {
return;
}
const optionsToOpen = this.getOptions(side);
if (!optionsToOpen) {
return;
}
/**
* If side is not set, we need to infer the side
* so we know which direction to move the options
*/
if (side === undefined) {
side = optionsToOpen === this.leftOptions ? 'start' : 'end';
}
// In RTL we want to switch the sides
side = isEndSide(side) ? 'end' : 'start';
const isStartOpen = this.openAmount < 0;
const isEndOpen = this.openAmount > 0;
/**
* If a side is open and a user tries to
* re-open the same side, we should not do anything
*/
if (isStartOpen && optionsToOpen === this.leftOptions) {
return;
}
if (isEndOpen && optionsToOpen === this.rightOptions) {
return;
}
this.closeOpened();
this.state = SlidingState.Enabled;
requestAnimationFrame(() => {
this.calculateOptsWidth();
const width = side === 'end' ? this.optsWidthRightSide : -this.optsWidthLeftSide;
openSlidingItem = this.el;
this.setOpenAmount(width, false);
this.state = side === 'end' ? SlidingState.End : SlidingState.Start;
});
}
/**
* Close the sliding item. Items can also be closed from the [List](./list).
*/
@Method()
async close() {
this.setOpenAmount(0, true);
}
/**
* Close all of the sliding items in the list. Items can also be closed from the [List](./list).
*/
@Method()
async closeOpened(): Promise<boolean> {
if (openSlidingItem !== undefined) {
openSlidingItem.close();
openSlidingItem = undefined;
return true;
}
return false;
}
/**
* Given an optional side, return the ion-item-options element.
*
* @param side This side of the options to get. If a side is not provided it will
* return the first one available.
*/
private getOptions(side?: string): HTMLIonItemOptionsElement | undefined {
if (side === undefined) {
return this.leftOptions || this.rightOptions;
} else if (side === 'start') {
return this.leftOptions;
} else {
return this.rightOptions;
}
}
private async updateOptions() {
const options = this.el.querySelectorAll('ion-item-options');
let sides = 0;
// Reset left and right options in case they were removed
this.leftOptions = this.rightOptions = undefined;
for (let i = 0; i < options.length; i++) {
const item = options.item(i);
/**
* We cannot use the componentOnReady helper
* util here since we need to wait for all of these items
* to be ready before we set `this.sides` and `this.optsDirty`.
*/
// eslint-disable-next-line custom-rules/no-component-on-ready-method
const option = (item as any).componentOnReady !== undefined ? await item.componentOnReady() : item;
const side = isEndSide(option.side) ? 'end' : 'start';
if (side === 'start') {
this.leftOptions = option;
sides |= ItemSide.Start;
} else {
this.rightOptions = option;
sides |= ItemSide.End;
}
}
this.optsDirty = true;
this.sides = sides;
}
private canStart(gesture: GestureDetail): boolean {
/**
* If very close to start of the screen
* do not open left side so swipe to go
* back will still work.
*/
const rtl = document.dir === 'rtl';
const atEdge = rtl ? window.innerWidth - gesture.startX < 15 : gesture.startX < 15;
if (atEdge) {
return false;
}
const selected = openSlidingItem;
if (selected && selected !== this.el) {
this.closeOpened();
}
return !!(this.rightOptions || this.leftOptions);
}
private onStart() {
/**
* We need to query for the ion-item
* every time the gesture starts. Developers
* may toggle ion-item elements via *ngIf.
*/
this.item = this.el.querySelector('ion-item');
const { contentEl } = this;
if (contentEl) {
this.initialContentScrollY = disableContentScrollY(contentEl);
}
openSlidingItem = this.el;
if (this.tmr !== undefined) {
clearTimeout(this.tmr);
this.tmr = undefined;
}
if (this.openAmount === 0) {
this.optsDirty = true;
this.state = SlidingState.Enabled;
}
this.initialOpenAmount = this.openAmount;
if (this.item) {
this.item.style.transition = 'none';
}
}
private onMove(gesture: GestureDetail) {
if (this.optsDirty) {
this.calculateOptsWidth();
}
let openAmount = this.initialOpenAmount - gesture.deltaX;
switch (this.sides) {
case ItemSide.End:
openAmount = Math.max(0, openAmount);
break;
case ItemSide.Start:
openAmount = Math.min(0, openAmount);
break;
case ItemSide.Both:
break;
case ItemSide.None:
return;
default:
printIonWarning('[ion-item-sliding] - invalid ItemSideFlags value', this.sides);
break;
}
let optsWidth;
if (openAmount > this.optsWidthRightSide) {
optsWidth = this.optsWidthRightSide;
openAmount = optsWidth + (openAmount - optsWidth) * ELASTIC_FACTOR;
} else if (openAmount < -this.optsWidthLeftSide) {
optsWidth = -this.optsWidthLeftSide;
openAmount = optsWidth + (openAmount - optsWidth) * ELASTIC_FACTOR;
}
this.setOpenAmount(openAmount, false);
}
private onEnd(gesture: GestureDetail) {
const { contentEl, initialContentScrollY } = this;
if (contentEl) {
resetContentScrollY(contentEl, initialContentScrollY);
}
const velocity = gesture.velocityX;
let restingPoint = this.openAmount > 0 ? this.optsWidthRightSide : -this.optsWidthLeftSide;
// Check if the drag didn't clear the buttons mid-point
// and we aren't moving fast enough to swipe open
const isResetDirection = this.openAmount > 0 === !(velocity < 0);
const isMovingFast = Math.abs(velocity) > 0.3;
const isOnCloseZone = Math.abs(this.openAmount) < Math.abs(restingPoint / 2);
if (swipeShouldReset(isResetDirection, isMovingFast, isOnCloseZone)) {
restingPoint = 0;
}
const state = this.state;
this.setOpenAmount(restingPoint, true);
if ((state & SlidingState.SwipeEnd) !== 0 && this.rightOptions) {
this.rightOptions.fireSwipeEvent();
} else if ((state & SlidingState.SwipeStart) !== 0 && this.leftOptions) {
this.leftOptions.fireSwipeEvent();
}
}
private calculateOptsWidth() {
this.optsWidthRightSide = 0;
if (this.rightOptions) {
this.rightOptions.style.display = 'flex';
this.optsWidthRightSide = this.rightOptions.offsetWidth;
this.rightOptions.style.display = '';
}
this.optsWidthLeftSide = 0;
if (this.leftOptions) {
this.leftOptions.style.display = 'flex';
this.optsWidthLeftSide = this.leftOptions.offsetWidth;
this.leftOptions.style.display = '';
}
this.optsDirty = false;
}
private setOpenAmount(openAmount: number, isFinal: boolean) {
if (this.tmr !== undefined) {
clearTimeout(this.tmr);
this.tmr = undefined;
}
if (!this.item) {
return;
}
const { el } = this;
const style = this.item.style;
this.openAmount = openAmount;
if (isFinal) {
style.transition = '';
}
if (openAmount > 0) {
this.state =
openAmount >= this.optsWidthRightSide + SWIPE_MARGIN
? SlidingState.End | SlidingState.SwipeEnd
: SlidingState.End;
} else if (openAmount < 0) {
this.state =
openAmount <= -this.optsWidthLeftSide - SWIPE_MARGIN
? SlidingState.Start | SlidingState.SwipeStart
: SlidingState.Start;
} else {
/**
* The sliding options should not be
* clickable while the item is closing.
*/
el.classList.add('item-sliding-closing');
/**
* Item sliding cannot be interrupted
* while closing the item. If it did,
* it would allow the item to get into an
* inconsistent state where multiple
* items are then open at the same time.
*/
if (this.gesture) {
this.gesture.enable(false);
}
this.tmr = setTimeout(() => {
this.state = SlidingState.Disabled;
this.tmr = undefined;
if (this.gesture) {
this.gesture.enable(!this.disabled);
}
el.classList.remove('item-sliding-closing');
}, 600);
openSlidingItem = undefined;
style.transform = '';
return;
}
style.transform = `translate3d(${-openAmount}px,0,0)`;
this.ionDrag.emit({
amount: openAmount,
ratio: this.getSlidingRatioSync(),
});
}
private getSlidingRatioSync(): number {
if (this.openAmount > 0) {
return this.openAmount / this.optsWidthRightSide;
} else if (this.openAmount < 0) {
return this.openAmount / this.optsWidthLeftSide;
} else {
return 0;
}
}
render() {
const mode = getIonMode(this);
return (
<Host
class={{
[mode]: true,
'item-sliding-active-slide': this.state !== SlidingState.Disabled,
'item-sliding-active-options-end': (this.state & SlidingState.End) !== 0,
'item-sliding-active-options-start': (this.state & SlidingState.Start) !== 0,
'item-sliding-active-swipe-end': (this.state & SlidingState.SwipeEnd) !== 0,
'item-sliding-active-swipe-start': (this.state & SlidingState.SwipeStart) !== 0,
}}
></Host>
);
}
}
const swipeShouldReset = (isResetDirection: boolean, isMovingFast: boolean, isOnResetZone: boolean): boolean => {
// The logic required to know when the sliding item should close (openAmount=0)
// depends on three booleans (isResetDirection, isMovingFast, isOnResetZone)
// and it ended up being too complicated to be written manually without errors
// so the truth table is attached below: (0=false, 1=true)
// isResetDirection | isMovingFast | isOnResetZone || shouldClose
// 0 | 0 | 0 || 0
// 0 | 0 | 1 || 1
// 0 | 1 | 0 || 0
// 0 | 1 | 1 || 0
// 1 | 0 | 0 || 0
// 1 | 0 | 1 || 1
// 1 | 1 | 0 || 1
// 1 | 1 | 1 || 1
// The resulting expression was generated by resolving the K-map (Karnaugh map):
return (!isMovingFast && isOnResetZone) || (isResetDirection && isMovingFast);
};