@gez/date-time-kit
Version:
265 lines (250 loc) • 8.37 kB
text/typescript
import {
autoUpdate,
computePosition,
flip,
offset,
size
} from '@floating-ui/dom';
import { html } from '../../utils';
import {
type BaseAttrs,
type BaseEmits,
type Emit2EventMap,
UiBase
} from '../web-component-base';
import { styleStr } from './css';
export type { reExportPopoverAttrs as reExportAttrs } from './attr-sync-helper';
export interface Attrs extends BaseAttrs {
open?: boolean;
disabled?: boolean;
/** @default 'bottom-start' */
placement?: `${'top' | 'bottom' | 'left' | 'right'}${'' | '-start' | '-end'}`;
/** @default 'none' */
strategy?: 'absolute' | 'fixed' | 'none';
/** @default 0 */
offset?: number;
'min-width-with-trigger'?: boolean;
}
export interface Emits extends BaseEmits {
'open-change': boolean;
}
export type EventMap = Emit2EventMap<Emits>;
const cacheStyle: {
-readonly [k in keyof CSSStyleDeclaration]?: any;
} = {};
let hiddenCount = 0;
const hiddenBodyOverflow = () => {
if (hiddenCount++) return;
const { style } = document.body;
(Array.from(style) as (keyof CSSStyleDeclaration)[]).forEach((prop) => {
cacheStyle[prop] = style[prop];
});
style.overflow = 'hidden';
};
const resetBodyOverflow = () => {
if (--hiddenCount > 0) return;
const { style } = document.body;
style.overflow = '';
for (const prop in cacheStyle) {
style[prop] = cacheStyle[prop];
Reflect.deleteProperty(cacheStyle, prop);
}
};
/**
* 点击触发器后气泡弹出
*/
export class Ele extends UiBase<Attrs, Emits> {
public static readonly tagName = 'dt-popover' as const;
protected static _style = styleStr;
protected static _template =
html`<slot name="trigger" part="trigger"></slot><slot name="pop" part="pop"></slot>`;
static get observedAttributes(): string[] {
return [
...(super.observedAttributes as (keyof BaseAttrs)[]),
'open',
'disabled',
'placement',
'strategy',
'offset',
'min-width-with-trigger'
] satisfies (keyof Attrs)[];
}
public get open() {
return this.hasAttribute('open');
}
public set open(v: boolean) {
this.toggleAttribute('open', v);
}
public get disabled() {
return this.hasAttribute('disabled');
}
public set disabled(v: boolean) {
this.toggleAttribute('disabled', v);
}
public get placement() {
return this._getAttr('placement', 'bottom-start');
}
public set placement(v: Attrs['placement']) {
if (v) this.setAttribute('placement', v);
else this.removeAttribute('placement');
}
public get strategy() {
return this._getAttr('strategy', 'none');
}
public set strategy(v: Attrs['strategy']) {
if (v) this.setAttribute('strategy', v);
else this.removeAttribute('strategy');
}
public get offset() {
const n = +this._getAttr('offset', '0');
return Number.isNaN(n) ? 0 : n;
}
public set offset(v: number) {
if (!Number.isNaN(v)) this.setAttribute('offset', v + '');
else this.removeAttribute('offset');
}
get _staticEls() {
return {
...super._staticEls,
pop: this.$0<HTMLSlotElement>`slot[name="pop"]`!,
trigger: this.$0<HTMLSlotElement>`slot[name="trigger"]`!
} as const;
}
private get _triggerAssignedEle() {
return this._els.trigger.assignedElements({ flatten: true })[0] as
| HTMLElement
| undefined;
}
private get _popAssignedEle() {
return (
(this._els.pop.assignedElements({ flatten: true })[0] as
| HTMLElement
| undefined) || this.querySelector<HTMLElement>('[slot="pop"]')
);
}
/**
* toggle open state
* @returns null if disabled, otherwise the new open state
*/
public toggleOpen = (force = !this.open) => {
if (this.disabled) return null;
return (this.open = force);
};
public connectedCallback() {
if (!super.connectedCallback()) return;
this._bindEvt(this._els.trigger)('click', this._onTriggerClick);
this.strategy = this.strategy;
}
public disconnectedCallback() {
this._cleanupAutoUpdate?.();
document.removeEventListener('click', this._onDocClick, true);
return super.disconnectedCallback();
}
protected _onAttrChanged(
name: string,
oldValue: string | null,
newValue: string | null
) {
super._onAttrChanged(name, oldValue, newValue);
if (name !== 'open') return;
const isOpen = newValue !== null;
setTimeout(() => {
document[(isOpen ? 'add' : 'remove') + 'EventListener'](
'click',
this._onDocClick,
true
);
});
if (!isOpen || this.strategy === 'none' || this._isSmallScreen)
this._cleanupAutoUpdate?.();
else this._autoUpdatePosition();
if (this._isSmallScreen) {
if (isOpen) hiddenBodyOverflow();
else resetBodyOverflow();
}
this.dispatchEvent('open-change', this.open, true);
}
private _onTriggerClick = () => {
this.toggleOpen();
};
private _onDocClick = (e: MouseEvent) => {
const popEle = this._popAssignedEle;
if (popEle) {
const composedPath = e.composedPath();
if (composedPath.includes(popEle)) return;
if (composedPath.includes(this)) {
const popRect = popEle.getBoundingClientRect();
if (
e.clientX >= popRect.left &&
e.clientX <= popRect.right &&
e.clientY >= popRect.top &&
e.clientY <= popRect.bottom
) {
return;
}
}
}
e.stopPropagation();
e.preventDefault();
this.open = false;
document.removeEventListener('click', this._onDocClick, true);
};
private _cleanupAutoUpdate: null | (() => void) = null;
private _autoUpdatePosition() {
this._cleanupAutoUpdate?.();
const updatePosition = async () => {
const { _triggerAssignedEle, _els, strategy } = this;
if (!_triggerAssignedEle || strategy === 'none') return;
const { x, y } = await computePosition(
_triggerAssignedEle,
_els.pop,
{
placement: this.placement,
strategy: strategy,
middleware: [
offset(this.offset),
flip(),
size({
apply: ({ elements, rects }) => {
if (
!this.hasAttribute('min-width-with-trigger')
)
return;
elements.floating.style.minWidth =
rects.reference.width + 'px';
}
})
]
}
);
function roundByDPR(value: number) {
const dpr = window.devicePixelRatio || 1;
return Math.round(value * dpr) / dpr;
}
_els.pop.style.transform = `translate(${roundByDPR(x)}px, ${roundByDPR(y)}px)`;
};
const cleanup = autoUpdate(
this._els.trigger,
this._els.pop,
updatePosition
);
this._cleanupAutoUpdate = () => {
cleanup();
this._cleanupAutoUpdate = null;
this._els.pop.style.transform = '';
};
}
protected _onScreenSizeChanged(isSmall: boolean) {
super._onScreenSizeChanged(isSmall);
if (!this.open || this.strategy === 'none') return;
if (isSmall) {
this._cleanupAutoUpdate?.();
this._els.pop.style.transform = '';
hiddenBodyOverflow();
} else {
this._autoUpdatePosition();
resetBodyOverflow();
}
}
}
Ele.define();