refactor: layout

This commit is contained in:
tanjinzhou 2021-05-26 14:47:50 +08:00
parent fe99051b55
commit 3825c6507f
9 changed files with 231 additions and 235 deletions

View File

@ -1,17 +1,25 @@
import classNames from '../_util/classNames';
import { inject, provide, PropType, defineComponent, nextTick } from 'vue';
import {
inject,
PropType,
defineComponent,
ExtractPropTypes,
ref,
watch,
onMounted,
onBeforeUnmount,
CSSProperties,
provide,
} from 'vue';
import PropTypes from '../_util/vue-types';
import { tuple } from '../_util/type';
import { getOptionProps, hasProp, getComponent, getSlot } from '../_util/props-util';
import initDefaultProps from '../_util/props-util/initDefaultProps';
import BaseMixin from '../_util/BaseMixin';
import isNumeric from '../_util/isNumeric';
import { defaultConfigProvider } from '../config-provider';
import BarsOutlined from '@ant-design/icons-vue/BarsOutlined';
import RightOutlined from '@ant-design/icons-vue/RightOutlined';
import LeftOutlined from '@ant-design/icons-vue/LeftOutlined';
import omit from 'omit.js';
import { SiderHookProvider } from './layout';
import useConfigInject from '../_util/hooks/useConfigInject';
import { SiderCollapsedKey, SiderHookProviderKey } from './injectionKey';
const dimensionMaxMap = {
xs: '479.98px',
@ -24,7 +32,7 @@ const dimensionMaxMap = {
export type CollapseType = 'clickTrigger' | 'responsive';
export const SiderProps = {
export const siderProps = {
prefixCls: PropTypes.string,
collapsible: PropTypes.looseBool,
collapsed: PropTypes.looseBool,
@ -40,6 +48,7 @@ export const SiderProps = {
onCollapse: Function as PropType<(collapsed: boolean, type: CollapseType) => void>,
};
export type SiderProps = Partial<ExtractPropTypes<typeof siderProps>>;
// export interface SiderState {
// collapsed?: boolean;
// below: boolean;
@ -61,10 +70,8 @@ const generateId = (() => {
export default defineComponent({
name: 'ALayoutSider',
mixins: [BaseMixin],
inheritAttrs: false,
__ANT_LAYOUT_SIDER: true,
props: initDefaultProps(SiderProps, {
props: initDefaultProps(siderProps, {
collapsible: false,
defaultCollapsed: false,
reverseArrow: false,
@ -72,173 +79,141 @@ export default defineComponent({
collapsedWidth: 80,
}),
emits: ['breakpoint', 'update:collapsed', 'collapse'],
setup() {
return {
siderHook: inject<SiderHookProvider>('siderHook', {}),
configProvider: inject('configProvider', defaultConfigProvider),
};
},
data() {
const uniqueId = generateId('ant-sider-');
let matchMedia: typeof window.matchMedia;
if (typeof window !== 'undefined') {
matchMedia = window.matchMedia;
}
const props = getOptionProps(this) as any;
let mql: MediaQueryList;
if (matchMedia && props.breakpoint && props.breakpoint in dimensionMaxMap) {
mql = matchMedia(`(max-width: ${dimensionMaxMap[props.breakpoint]})`);
}
let sCollapsed: boolean;
if ('collapsed' in props) {
sCollapsed = props.collapsed;
} else {
sCollapsed = props.defaultCollapsed;
}
return {
sCollapsed,
below: false,
belowShow: false,
uniqueId,
mql,
};
},
watch: {
collapsed(val) {
this.setState({
sCollapsed: val,
});
},
},
created() {
provide('layoutSiderContext', this); // menu使
},
mounted() {
nextTick(() => {
if (this.mql) {
this.mql.addListener(this.responsiveHandler);
this.responsiveHandler(this.mql);
}
if (this.siderHook.addSider) {
this.siderHook.addSider(this.uniqueId);
}
});
},
beforeUnmount() {
if (this.mql) {
this.mql.removeListener(this.responsiveHandler);
}
if (this.siderHook.removeSider) {
this.siderHook.removeSider(this.uniqueId);
}
},
methods: {
responsiveHandler(mql: MediaQueryListEvent | MediaQueryList) {
this.setState({ below: mql.matches });
this.$emit('breakpoint', mql.matches);
if (this.sCollapsed !== mql.matches) {
this.setCollapsed(mql.matches, 'responsive');
}
},
setCollapsed(collapsed: boolean, type: CollapseType) {
if (!hasProp(this, 'collapsed')) {
this.setState({
sCollapsed: collapsed,
});
}
this.$emit('update:collapsed', collapsed);
this.$emit('collapse', collapsed, type);
},
toggle() {
const collapsed = !this.sCollapsed;
this.setCollapsed(collapsed, 'clickTrigger');
},
belowShowChange() {
this.setState({ belowShow: !this.belowShow });
},
},
render() {
const {
prefixCls: customizePrefixCls,
class: className,
theme,
collapsible,
reverseArrow,
style,
width,
collapsedWidth,
zeroWidthTriggerStyle,
...others
} = { ...getOptionProps(this), ...this.$attrs } as any;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('layout-sider', customizePrefixCls);
const divProps = omit(others, [
'collapsed',
'defaultCollapsed',
'onCollapse',
'breakpoint',
'onBreakpoint',
'siderHook',
'zeroWidthTriggerStyle',
'trigger',
]);
const trigger = getComponent(this, 'trigger');
const rawWidth = this.sCollapsed ? collapsedWidth : width;
// use "px" as fallback unit for width
const siderWidth = isNumeric(rawWidth) ? `${rawWidth}px` : String(rawWidth);
// special trigger when collapsedWidth == 0
const zeroWidthTrigger =
parseFloat(String(collapsedWidth || 0)) === 0 ? (
<span
onClick={this.toggle}
class={`${prefixCls}-zero-width-trigger ${prefixCls}-zero-width-trigger-${
reverseArrow ? 'right' : 'left'
}`}
style={zeroWidthTriggerStyle}
>
<BarsOutlined />
</span>
) : null;
const iconObj = {
expanded: reverseArrow ? <RightOutlined /> : <LeftOutlined />,
collapsed: reverseArrow ? <LeftOutlined /> : <RightOutlined />,
};
const status = this.sCollapsed ? 'collapsed' : 'expanded';
const defaultTrigger = iconObj[status];
const triggerDom =
trigger !== null
? zeroWidthTrigger || (
<div class={`${prefixCls}-trigger`} onClick={this.toggle} style={{ width: siderWidth }}>
{trigger || defaultTrigger}
</div>
)
: null;
const divStyle = {
...style,
flex: `0 0 ${siderWidth}`,
maxWidth: siderWidth, // Fix width transition bug in IE11
minWidth: siderWidth, // https://github.com/ant-design/ant-design/issues/6349
width: siderWidth,
};
const siderCls = classNames(className, prefixCls, `${prefixCls}-${theme}`, {
[`${prefixCls}-collapsed`]: !!this.sCollapsed,
[`${prefixCls}-has-trigger`]: collapsible && trigger !== null && !zeroWidthTrigger,
[`${prefixCls}-below`]: !!this.below,
[`${prefixCls}-zero-width`]: parseFloat(siderWidth) === 0,
});
return (
<aside class={siderCls} {...divProps} style={divStyle}>
<div class={`${prefixCls}-children`}>{getSlot(this)}</div>
{collapsible || (this.below && zeroWidthTrigger) ? triggerDom : null}
</aside>
setup(props, { emit, attrs, slots }) {
const { prefixCls } = useConfigInject('layout-sider', props);
const siderHook = inject(SiderHookProviderKey);
const collapsed = ref(
!!(props.collapsed !== undefined ? props.collapsed : props.defaultCollapsed),
);
const below = ref(false);
watch(
() => props.collapsed,
() => {
collapsed.value = !!props.collapsed;
},
);
provide(SiderCollapsedKey, collapsed);
const handleSetCollapsed = (value: boolean, type: CollapseType) => {
if (props.collapsed === undefined) {
collapsed.value = value;
}
emit('update:collapsed', value);
emit('collapse', value, type);
};
// ========================= Responsive =========================
const responsiveHandlerRef = ref<(mql: MediaQueryListEvent | MediaQueryList) => void>(
(mql: MediaQueryListEvent | MediaQueryList) => {
below.value = mql.matches;
emit('breakpoint', mql.matches);
if (collapsed.value !== mql.matches) {
handleSetCollapsed(mql.matches, 'responsive');
}
},
);
let mql: MediaQueryList;
function responsiveHandler(mql: MediaQueryListEvent | MediaQueryList) {
return responsiveHandlerRef.value!(mql);
}
const uniqueId = generateId('ant-sider-');
onMounted(() => {
if (typeof window !== 'undefined') {
const { matchMedia } = window;
if (matchMedia! && props.breakpoint && props.breakpoint in dimensionMaxMap) {
mql = matchMedia(`(max-width: ${dimensionMaxMap[props.breakpoint]})`);
try {
mql.addEventListener('change', responsiveHandler);
} catch (error) {
mql.addListener(responsiveHandler);
}
responsiveHandler(mql);
}
}
siderHook && siderHook.addSider(uniqueId);
});
onBeforeUnmount(() => {
try {
mql?.removeEventListener('change', responsiveHandler);
} catch (error) {
mql?.removeListener(responsiveHandler);
}
siderHook && siderHook.removeSider(uniqueId);
});
const toggle = () => {
handleSetCollapsed(!collapsed.value, 'clickTrigger');
};
return () => {
const pre = prefixCls.value;
const {
collapsedWidth,
width,
reverseArrow,
zeroWidthTriggerStyle,
trigger,
collapsible,
theme,
} = props;
const rawWidth = collapsed.value ? collapsedWidth : width;
// use "px" as fallback unit for width
const siderWidth = isNumeric(rawWidth) ? `${rawWidth}px` : String(rawWidth);
// special trigger when collapsedWidth == 0
const zeroWidthTrigger =
parseFloat(String(collapsedWidth || 0)) === 0 ? (
<span
onClick={toggle}
class={classNames(
`${pre}-zero-width-trigger`,
`${pre}-zero-width-trigger-${reverseArrow ? 'right' : 'left'}`,
)}
style={zeroWidthTriggerStyle}
>
{trigger || <BarsOutlined />}
</span>
) : null;
const iconObj = {
expanded: reverseArrow ? <RightOutlined /> : <LeftOutlined />,
collapsed: reverseArrow ? <LeftOutlined /> : <RightOutlined />,
};
const status = collapsed.value ? 'collapsed' : 'expanded';
const defaultTrigger = iconObj[status];
const triggerDom =
trigger !== null
? zeroWidthTrigger || (
<div class={`${pre}-trigger`} onClick={toggle} style={{ width: siderWidth }}>
{trigger || defaultTrigger}
</div>
)
: null;
const divStyle = {
...(attrs.style as CSSProperties),
flex: `0 0 ${siderWidth}`,
maxWidth: siderWidth, // Fix width transition bug in IE11
minWidth: siderWidth, // https://github.com/ant-design/ant-design/issues/6349
width: siderWidth,
};
const siderCls = classNames(
pre,
`${pre}-${theme}`,
{
[`${pre}-collapsed`]: !!collapsed.value,
[`${pre}-has-trigger`]: collapsible && trigger !== null && !zeroWidthTrigger,
[`${pre}-below`]: !!below.value,
[`${pre}-zero-width`]: parseFloat(siderWidth) === 0,
},
attrs.class,
);
return (
<aside {...attrs} class={siderCls} style={divStyle} ref={ref}>
<div class={`${pre}-children`}>{slots.default?.()}</div>
{collapsible || (below && zeroWidthTrigger) ? triggerDom : null}
</aside>
);
};
},
});

View File

@ -2,6 +2,9 @@ import { App, Plugin } from 'vue';
import Layout from './layout';
import Sider from './Sider';
export { BasicProps as LayoutProps } from './layout';
export { SiderProps } from './Sider';
Layout.Sider = Sider;
/* istanbul ignore next */

View File

@ -0,0 +1,12 @@
import { Ref, InjectionKey } from 'vue';
export type SiderCollapsed = Ref<boolean>;
export const SiderCollapsedKey: InjectionKey<SiderCollapsed> = Symbol('siderCollapsed');
export interface SiderHookProvider {
addSider?: (id: string) => void;
removeSider?: (id: string) => void;
}
export const SiderHookProviderKey: InjectionKey<SiderHookProvider> = Symbol('siderHookProvider');

View File

@ -1,17 +1,8 @@
import {
createVNode,
defineComponent,
inject,
provide,
toRefs,
ref,
ExtractPropTypes,
HTMLAttributes,
} from 'vue';
import { createVNode, defineComponent, provide, ref, ExtractPropTypes, HTMLAttributes } from 'vue';
import PropTypes from '../_util/vue-types';
import classNames from '../_util/classNames';
import { defaultConfigProvider } from '../config-provider';
import { flattenChildren } from '../_util/props-util';
import useConfigInject from '../_util/hooks/useConfigInject';
import { SiderHookProviderKey } from './injectionKey';
export const basicProps = {
prefixCls: PropTypes.string,
@ -21,40 +12,29 @@ export const basicProps = {
export type BasicProps = Partial<ExtractPropTypes<typeof basicProps>> & HTMLAttributes;
export interface SiderHookProvider {
addSider?: (id: string) => void;
removeSider?: (id: string) => void;
}
type GeneratorArgument = {
suffixCls: string;
tagName: string;
tagName: 'header' | 'footer' | 'main' | 'section';
name: string;
};
function generator({ suffixCls, tagName, name }: GeneratorArgument) {
return (BasicComponent: typeof Basic) => {
const Adapter = defineComponent<BasicProps>({
const Adapter = defineComponent({
name,
props: basicProps,
setup(props, { slots }) {
const { getPrefixCls } = inject('configProvider', defaultConfigProvider);
const { prefixCls } = useConfigInject(suffixCls, props);
return () => {
const { prefixCls: customizePrefixCls } = props;
const prefixCls = getPrefixCls(suffixCls, customizePrefixCls);
const basicComponentProps = {
prefixCls,
...props,
prefixCls: prefixCls.value,
tagName,
...props,
};
return (
<BasicComponent {...basicComponentProps}>
{flattenChildren(slots.default?.())}
</BasicComponent>
);
return <BasicComponent {...basicComponentProps}>{slots.default?.()}</BasicComponent>;
};
},
});
Adapter.props = basicProps;
return Adapter;
};
}
@ -62,30 +42,32 @@ function generator({ suffixCls, tagName, name }: GeneratorArgument) {
const Basic = defineComponent({
props: basicProps,
setup(props, { slots }) {
const { prefixCls, tagName } = toRefs(props);
return () => createVNode(tagName.value, { class: prefixCls.value }, slots.default?.());
return () => createVNode(props.tagName, { class: props.prefixCls }, slots.default?.());
},
});
const BasicLayout = defineComponent({
props: basicProps,
setup(props, { slots }) {
const { direction } = useConfigInject('', props);
const siders = ref<string[]>([]);
const siderHookProvider: SiderHookProvider = {
addSider: id => {
const siderHookProvider = {
addSider: (id: string) => {
siders.value = [...siders.value, id];
},
removeSider: id => {
removeSider: (id: string) => {
siders.value = siders.value.filter(currentId => currentId !== id);
},
};
provide('siderHook', siderHookProvider);
provide(SiderHookProviderKey, siderHookProvider);
return () => {
const { prefixCls, hasSider, tagName } = props;
const divCls = classNames(prefixCls, {
[`${prefixCls}-has-sider`]:
typeof hasSider === 'boolean' ? hasSider : siders.value.length > 0,
[`${prefixCls}-rtl`]: direction.value === 'rtl',
});
return createVNode(tagName, { class: divCls }, slots.default?.());
};

View File

@ -2,6 +2,7 @@
@import '../../style/mixins/index';
@layout-prefix-cls: ~'@{ant-prefix}-layout';
@layout-menu-prefix-cls: ~'@{ant-prefix}-menu';
.@{layout-prefix-cls} {
display: flex;
@ -18,9 +19,10 @@
&&-has-sider {
flex-direction: row;
> .@{layout-prefix-cls},
> .@{layout-prefix-cls}-content {
overflow-x: hidden;
width: 0; // https://segmentfault.com/a/1190000019498300
}
}
@ -32,6 +34,7 @@
&-header {
height: @layout-header-height;
padding: @layout-header-padding;
color: @layout-header-color;
line-height: @layout-header-height;
background: @layout-header-background;
}
@ -64,6 +67,10 @@
// https://github.com/ant-design/ant-design/issues/7967
// solution from https://stackoverflow.com/a/33132624/3040605
padding-top: 0.1px;
.@{layout-menu-prefix-cls}.@{layout-menu-prefix-cls}-inline-collapsed {
width: auto;
}
}
&-has-trigger {
@ -88,7 +95,7 @@
}
&-zero-width {
& > * {
> * {
overflow: hidden;
}
@ -108,8 +115,19 @@
cursor: pointer;
transition: background 0.3s ease;
&:hover {
background: tint(@layout-sider-background, 10%);
&::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: transparent;
transition: all 0.3s;
content: '';
}
&:hover::after {
background: rgba(255, 255, 255, 0.1);
}
&-right {
@ -122,3 +140,4 @@
}
@import './light';
@import './rtl';

View File

@ -1,15 +1,11 @@
.@{layout-prefix-cls} {
&-sider {
&-light {
background: @layout-sider-background-light;
}
&-light &-trigger {
color: @layout-trigger-color-light;
background: @layout-trigger-background-light;
}
&-light &-zero-width-trigger {
color: @layout-trigger-color-light;
background: @layout-trigger-background-light;
}
.@{layout-prefix-cls}-sider-light {
background: @layout-sider-background-light;
.@{layout-prefix-cls}-sider-trigger {
color: @layout-trigger-color-light;
background: @layout-trigger-background-light;
}
.@{layout-prefix-cls}-sider-zero-width-trigger {
color: @layout-trigger-color-light;
background: @layout-trigger-background-light;
}
}

View File

@ -0,0 +1,10 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@layout-prefix-cls: ~'@{ant-prefix}-layout';
.@{layout-prefix-cls} {
&-rtl {
direction: rtl;
}
}

View File

@ -27,6 +27,7 @@ import {
import devWarning from '../../vc-util/devWarning';
import { collapseMotion, CSSMotionProps } from '../../_util/transition';
import uniq from 'lodash-es/uniq';
import { SiderCollapsedKey } from '../../layout/injectionKey';
export const menuProps = {
prefixCls: String,
@ -72,10 +73,7 @@ export default defineComponent({
setup(props, { slots, emit }) {
const { prefixCls, direction } = useConfigInject('menu', props);
const store = reactive<Record<string, StoreMenuInfo>>({});
const siderCollapsed = inject(
'layoutSiderCollapsed',
computed(() => undefined),
);
const siderCollapsed = inject(SiderCollapsedKey, ref(undefined));
const inlineCollapsed = computed(() => {
if (siderCollapsed.value !== undefined) {
return siderCollapsed.value;

View File

@ -300,10 +300,11 @@
// Layout
@layout-body-background: #f0f2f5;
@layout-header-background: #001529;
@layout-footer-background: @layout-body-background;
@layout-header-height: 64px;
@layout-header-padding: 0 50px;
@layout-header-color: @text-color;
@layout-footer-padding: 24px 50px;
@layout-footer-background: @layout-body-background;
@layout-sider-background: @layout-header-background;
@layout-trigger-height: 48px;
@layout-trigger-background: #002140;