Skip to content

Commit

Permalink
[duoyun-ui] Improve dy-contextmenu
Browse files Browse the repository at this point in the history
  • Loading branch information
mantou132 committed Mar 10, 2024
1 parent 83fa7b1 commit 3a9ae02
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 21 deletions.
60 changes: 48 additions & 12 deletions packages/duoyun-ui/src/elements/contextmenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ import type { DuoyunOptionsElement } from './options';

import './compartment';
import './button';
import './options';

export { SEPARATOR } from './options';

type Menu = ContextMenuItem[] | TemplateResult;
export interface ContextMenuItem {
/**`---` is separator */
text: string | TemplateResult;
description?: string | TemplateResult;
tag?: string | TemplateResult;
Expand All @@ -44,6 +44,8 @@ type ContextMenuStore = {
x: number;
y: number;
header?: TemplateResult;
causeEle?: HTMLDivElement;
causeMask?: TemplateResult;
}[];
};

Expand Down Expand Up @@ -203,12 +205,14 @@ export class DuoyunContextmenuElement extends GemElement {
#onEnterMenu = (evt: PointerEvent, menuStackIndex: number, subMenu?: Menu) => {
const { menuStack, openLeft } = contextmenuStore;
if (subMenu) {
const itemEle = evt.currentTarget as HTMLDivElement;
const { left, right, top, bottom, width } = itemEle.getBoundingClientRect();
const causeEle = evt.currentTarget as HTMLDivElement;
if (causeEle === menuStack.at(menuStackIndex + 1)?.causeEle) return;
const { left, right, top, bottom, width } = causeEle.getBoundingClientRect();
const em = parseFloat(getComputedStyle(this).fontSize);
const expectX = right - 0.75 * em;
const expectY = top - 0.4 * em;
const openTop = expectY > innerHeight - 150;
const predictSubMenuHeight = 240;
const openTop = expectY > innerHeight - predictSubMenuHeight;
const isToLeft =
(right + width > innerWidth ||
openLeft ||
Expand All @@ -218,6 +222,7 @@ export class DuoyunContextmenuElement extends GemElement {
menuStack: [
...menuStack.slice(0, menuStackIndex + 1),
{
causeEle,
openTop,
x: isToLeft ? left - width + 0.75 * em : expectX,
y: openTop ? innerHeight - bottom - 0.4 * em : expectY,
Expand Down Expand Up @@ -274,6 +279,36 @@ export class DuoyunContextmenuElement extends GemElement {
}
};

#genMask = async () => {
// await `<dy-options>` update
await Promise.resolve();
const causeEle = contextmenuStore.menuStack.at(-1)?.causeEle;
const optionsEle = this.optionsRef.elements.at(-1)!;
if (!causeEle) return;
const causeEleRect = causeEle.getBoundingClientRect();
const optionsEleRect = optionsEle.getBoundingClientRect();
const isRight = optionsEleRect.right > causeEleRect.right;
const startPointX = isRight ? causeEleRect.right : causeEleRect.left;
const startPoint = [startPointX + 50 * (isRight ? -1 : 1), (causeEleRect.top + causeEleRect.bottom) / 2];
const secondPointX = isRight ? optionsEleRect.left : optionsEleRect.right;
const secondPoint = [secondPointX, optionsEleRect.top];
const thirdPoint = [secondPointX, optionsEleRect.bottom];
contextmenuStore.menuStack.at(-1)!.causeMask = html`
<div
style=${styleMap({
background: 'rgba(0, 0, 0, 0.1)',
position: 'absolute',
width: `100vw`,
height: `100vh`,
clipPath: `polygon(${[startPoint, secondPoint, thirdPoint].map((point) =>
point.map((e) => `${e}px`).join(' '),
)})`,
})}
></div>
`;
update();
};

#onMaskClick = () => {
if (contextmenuStore.maskClosable) {
ContextMenu.close();
Expand All @@ -285,6 +320,10 @@ export class DuoyunContextmenuElement extends GemElement {
() => this.#menuElements.at(-1)?.focus(),
() => [contextmenuStore.menuStack.length],
);
this.effect(
() => this.#genMask(),
() => [contextmenuStore.menuStack.at(-1)?.menu],
);
const restoreInert = setBodyInert(this);
ContextMenu.instance = this;
this.addEventListener('contextmenu', this.#preventDefault);
Expand All @@ -304,21 +343,18 @@ export class DuoyunContextmenuElement extends GemElement {
<div class=${classMap({ mask: true, opaque: !maskClosable })} @click=${this.#onMaskClick}></div>
${menuStack.map(
(
{ x, y, menu, searchable, openTop, header },
{ x, y, menu, searchable, openTop, header, causeMask },
index,
_,
calcWidth = this.#width === 'auto' ? '0px' : this.#width,
) => html`
${causeMask}
<dy-options
class="menu"
ref=${this.optionsRef.ref}
style=${styleMap({
width: this.#width,
maxHeight: openTop
? '20em'
: maxHeight && index === 0
? `${maxHeight}`
: `calc(100vh - 0.8em - ${y - this.#offset}px)`,
maxHeight: maxHeight && index === 0 ? maxHeight : `calc(100vh - 0.8em - ${y - this.#offset}px)`,
[openTop ? 'bottom' : 'top']: `${y + this.#offset}px`,
left: `min(${x}px, calc(100vw - ${calcWidth} - ${2 * this.#offset}px))`,
})}
Expand All @@ -338,7 +374,7 @@ export class DuoyunContextmenuElement extends GemElement {
tag,
disabled,
danger,
highlight: !!(subMenu && subMenu === menuStack[index + 1]?.menu),
highlight: subMenu && subMenu === menuStack[index + 1]?.menu,
tagIcon: subMenu ? icons.right : selected ? icons.check : tagIcon,
onClick: subMenu ? onPointerEnter : onClick,
onPointerEnter,
Expand Down
4 changes: 3 additions & 1 deletion packages/duoyun-ui/src/elements/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ export type Adder = {
handle: (value: string) => any;
};

export const SEPARATOR = '---';

type State = {
search: string;
addValue: string;
Expand Down Expand Up @@ -266,7 +268,7 @@ export class DuoyunOptionsElement extends GemElement<State> {
onClick,
onRemove,
}) => {
return label === '---'
return label === SEPARATOR
? html`<div class="separator"></div>`
: html`
<div
Expand Down
6 changes: 3 additions & 3 deletions packages/duoyun-ui/src/elements/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
part,
} from '@mantou/gem/lib/decorators';
import { GemElement, html, TemplateResult } from '@mantou/gem/lib/element';
import { createCSSSheet, css, styleMap, StyleObject } from '@mantou/gem/lib/utils';
import { addListener, createCSSSheet, css, styleMap, StyleObject } from '@mantou/gem/lib/utils';

import { theme } from '../lib/theme';
import { icons } from '../lib/icons';
Expand Down Expand Up @@ -50,6 +50,7 @@ const style = createCSSSheet(css`
.inline-options {
max-height: inherit;
padding-block: 0;
overscroll-behavior: auto;
}
.value-wrap {
width: 0;
Expand Down Expand Up @@ -349,11 +350,10 @@ export class DuoyunSelectElement extends GemElement<State> implements BasePicker
maxHeight: isShowTop ? `${top - 8}px` : `calc(100vh - ${bottom + 8}px)`,
transform: isShowTop ? `translateY(calc(-100% - 8px - ${height}px))` : 'none',
});
addEventListener('pointerup', this.#close);
return addListener(window, 'pointerup', this.#close);
} else {
this.setState({ open: false });
}
return () => removeEventListener('pointerup', this.#close);
},
() => [this.state.open],
);
Expand Down
2 changes: 1 addition & 1 deletion packages/gem/src/lib/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function pushStaticField(target: GemElementPrototype, field: StaticField, member
(cls[field] as any)[isSet ? 'add' : 'push'](member);
}

export type RefObject<T = HTMLElement> = { ref: string; element: T | undefined };
export type RefObject<T = HTMLElement> = { ref: string; element: T | undefined; elements: T[] };

/**
* 引用元素,只有第一个标记 ref 的元素有效
Expand Down
12 changes: 9 additions & 3 deletions packages/gem/src/lib/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,11 @@ export function defineProperty(
});
}

const getReflectTargets = (ele: ShadowRoot | GemElement) =>
[...ele.querySelectorAll<GemReflectElement>('[data-gem-reflect]')].map((e) => e.target);

export function defineRef(target: GemElement, prop: string, ref: string) {
const refSelector = `[ref=${ref}]`;
Object.defineProperty(target, prop, {
configurable: true,
get() {
Expand All @@ -581,12 +585,14 @@ export function defineRef(target: GemElement, prop: string, ref: string) {
return ref;
},
get element() {
const gemReflects = [...ele.querySelectorAll<GemReflectElement>('[data-gem-reflect]')].map((e) => e.target);
for (const e of [ele, ...gemReflects]) {
const result = e.querySelector(`[ref=${ref}]`);
for (const e of [ele, ...getReflectTargets(ele)]) {
const result = e.querySelector(refSelector);
if (result) return result;
}
},
get elements() {
return [ele, ...getReflectTargets(ele)].map((e) => [...e.querySelectorAll(refSelector)]).flat();
},
};
proxy[prop] = refobject;
}
Expand Down
8 changes: 7 additions & 1 deletion packages/gem/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,13 @@ export function objectMapToString<T = any>(
}

// Wait: Typescript lib dom CSSStyleDeclaration anchor position
type StyleProp = keyof CSSStyleDeclaration | `--${string}` | 'anchorDefault' | 'anchorName';
type StyleProp =
| keyof CSSStyleDeclaration
| `--${string}`
| 'scrollbarWidth'
| 'scrollbarColor'
| 'anchorDefault'
| 'anchorName';

export type StyleObject = Partial<Record<StyleProp, string | number | false | undefined | null>>;

Expand Down

0 comments on commit 3a9ae02

Please sign in to comment.