Skip to content

Commit

Permalink
perf: perf context menu
Browse files Browse the repository at this point in the history
  • Loading branch information
anncwb committed Nov 25, 2020
1 parent 41d7900 commit 6e03e05
Show file tree
Hide file tree
Showing 16 changed files with 205 additions and 176 deletions.
62 changes: 1 addition & 61 deletions src/components/ContextMenu/index.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,2 @@
import contextMenuVue from './src/index';
import { isClient } from '/@/utils/is';
import { Options, Props } from './src/types';
import { createVNode, render } from 'vue';
const menuManager: {
domList: Element[];
resolve: Fn;
} = {
domList: [],
resolve: () => {},
};
export const createContextMenu = function (options: Options) {
const { event } = options || {};
try {
event.preventDefault();
} catch (e) {
console.log(e);
}

if (!isClient) return;
return new Promise((resolve) => {
const container = document.createElement('div');
const propsData: Partial<Props> = {};
if (options.styles !== undefined) propsData.styles = options.styles;
if (options.items !== undefined) propsData.items = options.items;
if (options.event !== undefined) {
propsData.customEvent = event;
propsData.axis = { x: event.clientX, y: event.clientY };
}
const vm = createVNode(contextMenuVue, propsData);
render(vm, container);
const bodyClick = function () {
menuManager.resolve('');
};
menuManager.domList.push(container);
const remove = function () {
menuManager.domList.forEach((dom: Element) => {
try {
document.body.removeChild(dom);
} catch (error) {}
});
document.body.removeEventListener('click', bodyClick);
document.body.removeEventListener('scroll', bodyClick);
};
menuManager.resolve = function (...arg: any) {
resolve(arg[0]);
remove();
};
remove();
document.body.appendChild(container);
document.body.addEventListener('click', bodyClick);
document.body.addEventListener('scroll', bodyClick);
});
};
export const unMountedContextMenu = function () {
if (menuManager) {
menuManager.resolve('');
menuManager.domList = [];
}
};

export { createContextMenu, destroyContextMenu } from './src/createContextMenu';
export * from './src/types';
73 changes: 73 additions & 0 deletions src/components/ContextMenu/src/createContextMenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import contextMenuVue from './index';
import { isClient } from '/@/utils/is';
import { CreateContextOptions, ContextMenuProps } from './types';
import { createVNode, render } from 'vue';

const menuManager: {
domList: Element[];
resolve: Fn;
} = {
domList: [],
resolve: () => {},
};

export const createContextMenu = function (options: CreateContextOptions) {
const { event } = options || {};

event && event?.preventDefault();

if (!isClient) return;
return new Promise((resolve) => {
const body = document.body;

const container = document.createElement('div');
const propsData: Partial<ContextMenuProps> = {};
if (options.styles) {
propsData.styles = options.styles;
}

if (options.items) {
propsData.items = options.items;
}

if (options.event) {
propsData.customEvent = event;
propsData.axis = { x: event.clientX, y: event.clientY };
}

const vm = createVNode(contextMenuVue, propsData);
render(vm, container);

const handleClick = function () {
menuManager.resolve('');
};

menuManager.domList.push(container);

const remove = function () {
menuManager.domList.forEach((dom: Element) => {
try {
dom && body.removeChild(dom);
} catch (error) {}
});
body.removeEventListener('click', handleClick);
body.removeEventListener('scroll', handleClick);
};

menuManager.resolve = function (...arg: any) {
remove();
resolve(arg[0]);
};
remove();
body.appendChild(container);
body.addEventListener('click', handleClick);
body.addEventListener('scroll', handleClick);
});
};

export const destroyContextMenu = function () {
if (menuManager) {
menuManager.resolve('');
menuManager.domList = [];
}
};
17 changes: 11 additions & 6 deletions src/components/ContextMenu/src/index.less
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
@import (reference) '../../../design/index.less';

@default-height: 42px !important;

@small-height: 36px !important;

@large-height: 36px !important;

.item-style() {
li {
display: inline-block;
width: 100%;
height: 46px !important;
height: @default-height;
margin: 0 !important;
line-height: 46px;
line-height: @default-height;

span {
line-height: 46px;
line-height: @default-height;
}

> div {
margin: 0 !important;
}

&:hover {
&:not(.ant-menu-item-disabled):hover {
color: @text-color-base;
background: #eee;
}
Expand All @@ -27,10 +33,9 @@
position: fixed;
top: 0;
left: 0;
z-index: 1500;
z-index: 200;
display: block;
width: 156px;
min-width: 10rem;
margin: 0;
list-style: none;
background-color: #fff;
Expand Down
136 changes: 68 additions & 68 deletions src/components/ContextMenu/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,111 +1,111 @@
import {
defineComponent,
nextTick,
onMounted,
reactive,
computed,
ref,
unref,
onUnmounted,
} from 'vue';
import './index.less';

import type { ContextMenuItem, ItemContentProps } from './types';
import type { FunctionalComponent, CSSProperties } from 'vue';

import { defineComponent, nextTick, onMounted, computed, ref, unref, onUnmounted } from 'vue';

import { props } from './props';
import Icon from '/@/components/Icon';
import { Menu, Divider } from 'ant-design-vue';

import type { ContextMenuItem } from './types';
import { props } from './props';

import './index.less';
const prefixCls = 'context-menu';

const ItemContent: FunctionalComponent<ItemContentProps> = (props) => {
const { item } = props;
return (
<span style="display: inline-block; width: 100%;" onClick={props.handler.bind(null, item)}>
{props.showIcon && item.icon && <Icon class="mr-2" icon={item.icon} />}
<span>{item.label}</span>
</span>
);
};

export default defineComponent({
name: 'ContextMenu',
props,
setup(props) {
const wrapRef = ref<Nullable<HTMLDivElement>>(null);
const state = reactive({
show: false,
});
const wrapRef = ref<ElRef>(null);
const showRef = ref(false);

const getStyle = computed(
(): CSSProperties => {
const { axis, items, styles, width } = props;
const { x, y } = axis || { x: 0, y: 0 };
const menuHeight = (items || []).length * 40;
const menuWidth = width;
const body = document.body;

const left = body.clientWidth < x + menuWidth ? x - menuWidth : x;
const top = body.clientHeight < y + menuHeight ? y - menuHeight : y;
return {
...styles,
width: `${width}px`,
left: `${left + 1}px`,
top: `${top + 1}px`,
};
}
);

onMounted(() => {
nextTick(() => {
state.show = true;
});
nextTick(() => (showRef.value = true));
});

onUnmounted(() => {
const el = unref(wrapRef);
el && document.body.removeChild(el);
});
const getStyle = computed(() => {
const { axis, items, styles, width } = props;
const { x, y } = axis || { x: 0, y: 0 };
const menuHeight = (items || []).length * 40;
const menuWidth = width;
const body = document.body;
return {
...(styles as any),
width: `${width}px`,
left: (body.clientWidth < x + menuWidth ? x - menuWidth : x) + 'px',
top: (body.clientHeight < y + menuHeight ? y - menuHeight : y) + 'px',
};
});

function handleAction(item: ContextMenuItem, e: MouseEvent) {
state.show = false;
const { handler, disabled } = item;
if (disabled) {
return;
}
if (e) {
e.stopPropagation();
e.preventDefault();
}
if (disabled) return;
showRef.value = false;

handler && handler();
}

function renderContent(item: ContextMenuItem) {
const { icon, label } = item;

const { showIcon } = props;
return (
<span style="display: inline-block; width: 100%;" onClick={handleAction.bind(null, item)}>
{showIcon && icon && <Icon class="mr-2" icon={icon} />}
<span>{label}</span>
</span>
);
e?.stopPropagation();
e?.preventDefault();
handler?.();
}

function renderMenuItem(items: ContextMenuItem[]) {
return items.map((item, index) => {
return items.map((item) => {
const { disabled, label, children, divider = false } = item;

const DividerComp = divider ? <Divider key={`d-${index}`} /> : null;
const DividerComp = divider ? <Divider key={`d-${label}`} /> : null;
if (!children || children.length === 0) {
return [
<Menu.Item disabled={disabled} class={`${prefixCls}__item`} key={label}>
{() => [renderContent(item)]}
</Menu.Item>,
DividerComp,
];
return (
<>
<Menu.Item disabled={disabled} class={`${prefixCls}__item`} key={label}>
{() => [
<ItemContent showIcon={props.showIcon} item={item} handler={handleAction} />,
]}
</Menu.Item>
{DividerComp}
</>
);
}
return !state.show ? null : (
<Menu.SubMenu key={label} disabled={disabled} popupClassName={`${prefixCls}__popup `}>
if (!unref(showRef)) return null;

return (
<Menu.SubMenu key={label} disabled={disabled} popupClassName={`${prefixCls}__popup`}>
{{
title: () => renderContent(item),
default: () => [renderMenuItem(children)],
title: () => (
<ItemContent showIcon={props.showIcon} item={item} handler={handleAction} />
),
default: () => renderMenuItem(children),
}}
</Menu.SubMenu>
);
});
}
return () => {
const { items } = props;
return !state.show ? null : (
if (!unref(showRef)) return null;
return (
<Menu
inlineIndent={12}
mode="vertical"
class={[prefixCls]}
class={prefixCls}
ref={wrapRef}
style={unref(getStyle)}
>
Expand Down
10 changes: 3 additions & 7 deletions src/components/ContextMenu/src/props.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import type { PropType } from 'vue';
import type { PropType, CSSProperties } from 'vue';
import type { Axis, ContextMenuItem } from './types';
export const props = {
width: {
type: Number as PropType<number>,
default: 180,
default: 156,
},
customEvent: {
type: Object as PropType<Event>,
default: null,
},
styles: {
type: Object as PropType<any>,
type: Object as PropType<CSSProperties>,
default: null,
},
showIcon: {
Expand All @@ -31,8 +31,4 @@ export const props = {
return [];
},
},
resolve: {
type: Function as PropType<any>,
default: null,
},
};

0 comments on commit 6e03e05

Please sign in to comment.