refactor: Anchor、Alert、Avatar、Badge、BackTop、Col、Form、Layout、Menu、Space、Spin、Switch、Row、Result、Rate (#4171)
* chore: remove resize-observer-polyfill * refactor: align * refactor(v3/avatar): refactor using composition api (#4052) * refactor(avatar): refactor using composition api * refactor: update props define * fix: avatar src scale not update * refactor: resizeObserver * refactor: divider * refactor: localeProvider * refactor(v3/back-top): use composition api (#4060) * refactor: backtop * refactor: empty * refactor: transButton * feat(v3/avatar): add avatar group (#4062) * feat(avatar): add avatar group * refactor: update * refactor: update Co-authored-by: tangjinzhou <415800467@qq.com> * refactor: avatar * refactor: avatar * style: rename useProvide * refactor: menu (#4110) * fix: menu * refactor: menu * refactor: remove rc-menu * fix: menu rtl error * style: lint * refactor(Anchor): use composition api (#4054) * refactor: anchor * refactor: anchor * refactor: anchor * feat: update * fix: icon class lose * refactor(v3/badge): use composition api (#4076) * refactor: badge * fix: badge inheritAttrs * refactor: grid * refactor: layout * fix: menu not close * refactor: space * refactor: result * refactor: affix * refactor: comment * refactor: form * feat: spin add rtl * feat: export spin type * refactor: pageHeader * refactor: page-header * refactor: skeleton * refactor: typography * refactor(v3/rate): use composition api * fix: add useRef hook * refactor: form * fix: menu not update * refactor: form * refactor: form * fix: slide animate not work * fix: menu mode error * fix: menu icon * refactor: rate * perf: remove rate * feat: add vc-overflow * refactor: menu * fix: remove flex check (#4165) * fix: dist locale file lose #3684 * release 2.2.0-beta.1 * dcos: update changelog * chore: update type * docs: update changelog Co-authored-by: John <John60676@qq.com> Co-authored-by: 言肆 <18x@loacg.com> Co-authored-by: zkwolf <chenhao5866@gmail.com>pull/4175/head 2.2.0-beta.1
parent
b91659e4f7
commit
9e0df41a55
|
@ -10,6 +10,37 @@
|
|||
|
||||
---
|
||||
|
||||
## 2.2.0-beta.1
|
||||
|
||||
`2021-06-17`
|
||||
|
||||
- 🔥🔥🔥 Virtual Table independent library released https://www.npmjs.com/package/@surely-vue/table, this component is an independent library, the document example is not yet complete, it is a completely ts-developed component , There are good type hints, there are API documents on npm, those who are in a hurry can explore and use it, here is an online experience example, https://store.antdv.com/pro/preview/list/big- table-list
|
||||
- 🔥🔥🔥 Refactored a large number of components, the source code is more readable, the performance is better, and the ts type is more comprehensive -Refactored components in this version Anchor, Alert, Avatar, Badge, BackTop, Col, Form, Layout, Menu, Space, Spin, Switch, Row, Result, Rate
|
||||
- 🎉 Menu
|
||||
|
||||
- Better performance [#3300](https://github.com/vueComponent/ant-design-vue/issues/3300)
|
||||
- Fix the problem of incorrect highlighting [#4053](https://github.com/vueComponent/ant-design-vue/issues/4053)
|
||||
- Fix console invalid warning [#4169](https://github.com/vueComponent/ant-design-vue/issues/4169)
|
||||
- Easier to use, simpler to use single file recursion [#4133](https://github.com/vueComponent/ant-design-vue/issues/4133)
|
||||
- 💄 icon icon needs to be passed through slot
|
||||
|
||||
- Skeleton
|
||||
|
||||
- 🌟 Support Skeleton.Avatar placeholder component.
|
||||
- 🌟 Support Skeleton.Button placeholder component.
|
||||
- 🌟 Support Skeleton.Input placeholder component.
|
||||
|
||||
- 💄 Destructive update
|
||||
|
||||
- The `a-menu-item` and `a-sub-menu` icons need to be passed through the slot, and the icon is not automatically obtained through the sub-node
|
||||
- row gutter supports row-wrap, no need to use multiple rows to divide col
|
||||
- `Menu` removes `defaultOpenKeys` and `defaultSelectedKeys`; `Switch` removes `defaultChecked`; `Rate` removes `defaultValue`; Please be cautious to use the defaultXxx-named attributes of other unrefactored components, and they will be removed in future versions.
|
||||
|
||||
- 🌟 Added Avatar.Group component
|
||||
- 🐞 Fix AutoComplete filterOptions not taking effect [#4170](https://github.com/vueComponent/ant-design-vue/issues/4170)
|
||||
- 🐞 Fix Select automatic width invalidation problem [#4118](https://github.com/vueComponent/ant-design-vue/issues/4118)
|
||||
- 🐞 Fix the lack of internationalized files in dist [#3684](https://github.com/vueComponent/ant-design-vue/issues/3684)
|
||||
|
||||
## 2.1.6
|
||||
|
||||
`2021-05-13`
|
||||
|
|
|
@ -10,6 +10,38 @@
|
|||
|
||||
---
|
||||
|
||||
## 2.2.0-beta.1
|
||||
|
||||
`2021-06-17`
|
||||
|
||||
- 🔥🔥🔥 虚拟 Table 独立库发布 https://www.npmjs.com/package/@surely-vue/table , 该组件是一个独立的库,目前文档示例尚未完善,他是一个完全 ts 开发的组件,有较好的类型提示,npm 上已有 API 文档,着急使用的的可以摸索着用起来了,这里有个在线体验示例,https://store.antdv.com/pro/preview/list/big-table-list
|
||||
- 🔥🔥🔥 重构大量组件,源码更加易读,性能更优,ts 类型更加全面
|
||||
- 本版本重构组件 Anchor、Alert、Avatar、Badge、BackTop、Col、Form、Layout、Menu、Space、Spin、Switch、Row、Result、Rate
|
||||
- 🎉 Menu
|
||||
|
||||
- 性能更优 [#3300](https://github.com/vueComponent/ant-design-vue/issues/3300)
|
||||
- 修复高亮不正确问题 [#4053](https://github.com/vueComponent/ant-design-vue/issues/4053)
|
||||
- 修复控制台无效 warning [#4169](https://github.com/vueComponent/ant-design-vue/issues/4169)
|
||||
- 更加易用,更加简单的使用单文件递归 [#4133](https://github.com/vueComponent/ant-design-vue/issues/4133)
|
||||
- 💄 图标 icon 需要通过 slot 传递
|
||||
|
||||
- Skeleton
|
||||
|
||||
- 🌟 支持 Skeleton.Avatar 占位组件。
|
||||
- 🌟 支持 Skeleton.Button 占位组件。
|
||||
- 🌟 支持 Skeleton.Input 占位组件。
|
||||
|
||||
- 💄 破坏性更新
|
||||
|
||||
- `a-menu-item`、`a-sub-menu` 图标需要通过 slot 传递,不在通过子节点自动获取图标
|
||||
- row gutter 支持 row-wrap, 无需使用多个 row 划分 col
|
||||
- Menu 移除 defaultOpenKeys、defaultSelectedKeys; Switch 移除 defaultChecked; Rate 移除 defaultValue; 其它未重构组件的 defaultXxx 命名的属性请谨慎使用,在未来的版本中也会被移除。
|
||||
|
||||
- 🌟 新增 Avatar.Group 组件
|
||||
- 🐞 修复 AutoComplete filterOptions 不生效问题 [#4170](https://github.com/vueComponent/ant-design-vue/issues/4170)
|
||||
- 🐞 修复 Select 自动宽度失效问题 [#4118](https://github.com/vueComponent/ant-design-vue/issues/4118)
|
||||
- 🐞 修复 dist 缺少国际化文件问题 [#3684](https://github.com/vueComponent/ant-design-vue/issues/3684)
|
||||
|
||||
## 2.1.6
|
||||
|
||||
`2021-05-13`
|
||||
|
|
|
@ -294,7 +294,7 @@ gulp.task(
|
|||
|
||||
function publish(tagString, done) {
|
||||
let args = ['publish', '--with-antd-tools'];
|
||||
args = args.concat(['--tag', 'next']);
|
||||
// args = args.concat(['--tag', 'next']);
|
||||
if (tagString) {
|
||||
args = args.concat(['--tag', tagString]);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
function canUseDom() {
|
||||
return !!(typeof window !== 'undefined' && window.document && window.document.createElement);
|
||||
}
|
||||
|
||||
export default canUseDom;
|
|
@ -1,17 +0,0 @@
|
|||
export default function getScroll(target, top) {
|
||||
if (typeof window === 'undefined') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const prop = top ? 'pageYOffset' : 'pageXOffset';
|
||||
const method = top ? 'scrollTop' : 'scrollLeft';
|
||||
const isWindow = target === window;
|
||||
|
||||
let ret = isWindow ? target[prop] : target[method];
|
||||
// ie6,7,8 standard mode
|
||||
if (isWindow && typeof ret !== 'number') {
|
||||
ret = window.document.documentElement[method];
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
export function isWindow(obj: any) {
|
||||
return obj !== null && obj !== undefined && obj === obj.window;
|
||||
}
|
||||
|
||||
export default function getScroll(
|
||||
target: HTMLElement | Window | Document | null,
|
||||
top: boolean,
|
||||
): number {
|
||||
if (typeof window === 'undefined') {
|
||||
return 0;
|
||||
}
|
||||
const method = top ? 'scrollTop' : 'scrollLeft';
|
||||
let result = 0;
|
||||
if (isWindow(target)) {
|
||||
result = (target as Window)[top ? 'pageYOffset' : 'pageXOffset'];
|
||||
} else if (target instanceof Document) {
|
||||
result = target.documentElement[method];
|
||||
} else if (target) {
|
||||
result = (target as HTMLElement)[method];
|
||||
}
|
||||
if (target && !isWindow(target) && typeof result !== 'number') {
|
||||
result = ((target as HTMLElement).ownerDocument || (target as Document)).documentElement?.[
|
||||
method
|
||||
];
|
||||
}
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { onMounted, onUnmounted, Ref, ref } from 'vue';
|
||||
import ResponsiveObserve, { ScreenMap } from '../../_util/responsiveObserve';
|
||||
|
||||
function useBreakpoint(): Ref<ScreenMap> {
|
||||
const screens = ref<ScreenMap>({});
|
||||
let token = null;
|
||||
|
||||
onMounted(() => {
|
||||
token = ResponsiveObserve.subscribe(supportScreens => {
|
||||
screens.value = supportScreens;
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
ResponsiveObserve.unsubscribe(token);
|
||||
});
|
||||
|
||||
return screens;
|
||||
}
|
||||
|
||||
export default useBreakpoint;
|
|
@ -1,8 +1,46 @@
|
|||
import { computed, inject } from 'vue';
|
||||
import { defaultConfigProvider } from '../../config-provider';
|
||||
import { RequiredMark } from '../../form/Form';
|
||||
import { computed, ComputedRef, inject, UnwrapRef } from 'vue';
|
||||
import {
|
||||
ConfigProviderProps,
|
||||
defaultConfigProvider,
|
||||
Direction,
|
||||
SizeType,
|
||||
} from '../../config-provider';
|
||||
|
||||
export default (name: string, props: Record<any, any>) => {
|
||||
const configProvider = inject('configProvider', defaultConfigProvider);
|
||||
export default (
|
||||
name: string,
|
||||
props: Record<any, any>,
|
||||
): {
|
||||
configProvider: UnwrapRef<ConfigProviderProps>;
|
||||
prefixCls: ComputedRef<string>;
|
||||
direction: ComputedRef<Direction>;
|
||||
size: ComputedRef<SizeType>;
|
||||
getTargetContainer: ComputedRef<() => HTMLElement>;
|
||||
space: ComputedRef<{ size: SizeType | number }>;
|
||||
pageHeader: ComputedRef<{ ghost: boolean }>;
|
||||
form?: ComputedRef<{
|
||||
requiredMark?: RequiredMark;
|
||||
}>;
|
||||
} => {
|
||||
const configProvider = inject<UnwrapRef<ConfigProviderProps>>(
|
||||
'configProvider',
|
||||
defaultConfigProvider,
|
||||
);
|
||||
const prefixCls = computed(() => configProvider.getPrefixCls(name, props.prefixCls));
|
||||
return { configProvider, prefixCls };
|
||||
const direction = computed(() => configProvider.direction);
|
||||
const space = computed(() => configProvider.space);
|
||||
const pageHeader = computed(() => configProvider.pageHeader);
|
||||
const form = computed(() => configProvider.form);
|
||||
const size = computed(() => props.size || configProvider.componentSize);
|
||||
const getTargetContainer = computed(() => props.getTargetContainer);
|
||||
return {
|
||||
configProvider,
|
||||
prefixCls,
|
||||
direction,
|
||||
size,
|
||||
getTargetContainer,
|
||||
space,
|
||||
pageHeader,
|
||||
form,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { onMounted, ref } from 'vue';
|
||||
import { detectFlexGapSupported } from '../styleChecker';
|
||||
|
||||
export default () => {
|
||||
const flexible = ref(false);
|
||||
onMounted(() => {
|
||||
flexible.value = detectFlexGapSupported();
|
||||
});
|
||||
|
||||
return flexible;
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
import { computed, ComputedRef, inject } from 'vue';
|
||||
import { defaultConfigProvider } from '../../config-provider';
|
||||
|
||||
export default (name: string, props: Record<any, any>): ComputedRef<string> => {
|
||||
const configProvider = inject('configProvider', defaultConfigProvider);
|
||||
const prefixCls = computed(() => configProvider.getPrefixCls(name, props.prefixCls));
|
||||
return prefixCls;
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
import { onBeforeUpdate, ref, Ref } from 'vue';
|
||||
|
||||
export type UseRef = [(el: any, key: string | number) => void, Ref<any>];
|
||||
|
||||
export const useRef = (): UseRef => {
|
||||
const refs = ref<any>({});
|
||||
const setRef = (el: any, key: string | number) => {
|
||||
refs.value[key] = el;
|
||||
};
|
||||
onBeforeUpdate(() => {
|
||||
refs.value = {};
|
||||
});
|
||||
return [setRef, refs];
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
import { computed, ComputedRef, inject, provide, UnwrapRef } from 'vue';
|
||||
import { ConfigProviderProps, defaultConfigProvider, SizeType } from '../../config-provider';
|
||||
|
||||
const sizeProvider = Symbol('SizeProvider');
|
||||
|
||||
const useProvideSize = <T = SizeType>(props: Record<any, any>): ComputedRef<T> => {
|
||||
const configProvider = inject<UnwrapRef<ConfigProviderProps>>(
|
||||
'configProvider',
|
||||
defaultConfigProvider,
|
||||
);
|
||||
const size = computed<T>(() => props.size || configProvider.componentSize);
|
||||
provide(sizeProvider, size);
|
||||
return size;
|
||||
};
|
||||
|
||||
const useInjectSize = <T = SizeType>(props?: Record<any, any>): ComputedRef<T> => {
|
||||
const size: ComputedRef<T> = props
|
||||
? computed(() => props.size)
|
||||
: inject(
|
||||
sizeProvider,
|
||||
computed(() => ('default' as unknown) as T),
|
||||
);
|
||||
return size;
|
||||
};
|
||||
|
||||
export { useInjectSize, sizeProvider, useProvideSize };
|
||||
|
||||
export default useProvideSize;
|
|
@ -116,7 +116,7 @@ const getSlotOptions = () => {
|
|||
throw Error('使用 .type 直接取值');
|
||||
};
|
||||
const findDOMNode = instance => {
|
||||
let node = instance && (instance.$el || instance);
|
||||
let node = instance?.vnode?.el || (instance && (instance.$el || instance));
|
||||
while (node && !node.tagName) {
|
||||
node = node.nextSibling;
|
||||
}
|
||||
|
@ -394,7 +394,7 @@ function isValidElement(element) {
|
|||
}
|
||||
|
||||
function getPropsSlot(slots, props, prop = 'default') {
|
||||
return slots[prop]?.() ?? props[prop];
|
||||
return props[prop] ?? slots[prop]?.();
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export type Breakpoint = 'xxl' | 'xl' | 'lg' | 'md' | 'sm' | 'xs';
|
||||
export type BreakpointMap = Partial<Record<Breakpoint, string>>;
|
||||
export type BreakpointMap = Record<Breakpoint, string>;
|
||||
export type ScreenMap = Partial<Record<Breakpoint, boolean>>;
|
||||
export type ScreenSizeMap = Partial<Record<Breakpoint, number>>;
|
||||
|
||||
export const responsiveArray: Breakpoint[] = ['xxl', 'xl', 'lg', 'md', 'sm', 'xs'];
|
||||
|
||||
|
@ -43,7 +44,7 @@ const responsiveObserve = {
|
|||
},
|
||||
unregister() {
|
||||
Object.keys(responsiveMap).forEach((screen: string) => {
|
||||
const matchMediaQuery = responsiveMap[screen]!;
|
||||
const matchMediaQuery = responsiveMap[screen];
|
||||
const handler = this.matchHandlers[matchMediaQuery];
|
||||
handler?.mql.removeListener(handler?.listener);
|
||||
});
|
||||
|
@ -51,7 +52,7 @@ const responsiveObserve = {
|
|||
},
|
||||
register() {
|
||||
Object.keys(responsiveMap).forEach((screen: string) => {
|
||||
const matchMediaQuery = responsiveMap[screen]!;
|
||||
const matchMediaQuery = responsiveMap[screen];
|
||||
const listener = ({ matches }: { matches: boolean }) => {
|
||||
this.dispatch({
|
||||
...screens,
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import getScroll from './getScroll';
|
||||
import raf from './raf';
|
||||
import getScroll, { isWindow } from './getScroll';
|
||||
import { easeInOutCubic } from './easings';
|
||||
|
||||
interface ScrollToOptions {
|
||||
/** Scroll container, default as window */
|
||||
getContainer?: () => HTMLElement | Window;
|
||||
getContainer?: () => HTMLElement | Window | Document;
|
||||
/** Scroll end callback */
|
||||
callback?: () => any;
|
||||
/** Animation duration, default as 450 */
|
||||
|
@ -12,7 +13,6 @@ interface ScrollToOptions {
|
|||
|
||||
export default function scrollTo(y: number, options: ScrollToOptions = {}) {
|
||||
const { getContainer = () => window, callback, duration = 450 } = options;
|
||||
|
||||
const container = getContainer();
|
||||
const scrollTop = getScroll(container, true);
|
||||
const startTime = Date.now();
|
||||
|
@ -21,16 +21,18 @@ export default function scrollTo(y: number, options: ScrollToOptions = {}) {
|
|||
const timestamp = Date.now();
|
||||
const time = timestamp - startTime;
|
||||
const nextScrollTop = easeInOutCubic(time > duration ? duration : time, scrollTop, y, duration);
|
||||
if (container === window) {
|
||||
window.scrollTo(window.pageXOffset, nextScrollTop);
|
||||
if (isWindow(container)) {
|
||||
(container as Window).scrollTo(window.pageXOffset, nextScrollTop);
|
||||
} else if (container instanceof HTMLDocument || container.constructor.name === 'HTMLDocument') {
|
||||
(container as HTMLDocument).documentElement.scrollTop = nextScrollTop;
|
||||
} else {
|
||||
(container as HTMLElement).scrollTop = nextScrollTop;
|
||||
}
|
||||
if (time < duration) {
|
||||
requestAnimationFrame(frameFunc);
|
||||
raf(frameFunc);
|
||||
} else if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
requestAnimationFrame(frameFunc);
|
||||
raf(frameFunc);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
const isStyleSupport = (styleName: string | Array<string>): boolean => {
|
||||
if (typeof window !== 'undefined' && window.document && window.document.documentElement) {
|
||||
import canUseDom from './canUseDom';
|
||||
|
||||
export const canUseDocElement = () => canUseDom() && window.document.documentElement;
|
||||
|
||||
export const isStyleSupport = (styleName: string | Array<string>): boolean => {
|
||||
if (canUseDocElement()) {
|
||||
const styleNameList = Array.isArray(styleName) ? styleName : [styleName];
|
||||
const { documentElement } = window.document;
|
||||
|
||||
|
@ -8,6 +12,32 @@ const isStyleSupport = (styleName: string | Array<string>): boolean => {
|
|||
return false;
|
||||
};
|
||||
|
||||
export const isFlexSupported = isStyleSupport(['flex', 'webkitFlex', 'Flex', 'msFlex']);
|
||||
let flexGapSupported: boolean | undefined;
|
||||
export const detectFlexGapSupported = () => {
|
||||
if (!canUseDocElement()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (flexGapSupported !== undefined) {
|
||||
return flexGapSupported;
|
||||
}
|
||||
|
||||
// create flex container with row-gap set
|
||||
const flex = document.createElement('div');
|
||||
flex.style.display = 'flex';
|
||||
flex.style.flexDirection = 'column';
|
||||
flex.style.rowGap = '1px';
|
||||
|
||||
// create two, elements inside it
|
||||
flex.appendChild(document.createElement('div'));
|
||||
flex.appendChild(document.createElement('div'));
|
||||
|
||||
// append to the DOM (needed to obtain scrollHeight)
|
||||
document.body.appendChild(flex);
|
||||
flexGapSupported = flex.scrollHeight === 1; // flex container should be 1px high from the row-gap
|
||||
document.body.removeChild(flex);
|
||||
|
||||
return flexGapSupported;
|
||||
};
|
||||
|
||||
export default isStyleSupport;
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
import { defineComponent } from 'vue';
|
||||
/**
|
||||
* Wrap of sub component which need use as Button capacity (like Icon component).
|
||||
* This helps accessibility reader to tread as a interactive button to operation.
|
||||
*/
|
||||
import KeyCode from './KeyCode';
|
||||
import PropTypes from './vue-types';
|
||||
|
||||
const inlineStyle = {
|
||||
border: 0,
|
||||
background: 'transparent',
|
||||
padding: 0,
|
||||
lineHeight: 'inherit',
|
||||
display: 'inline-block',
|
||||
};
|
||||
|
||||
const TransButton = defineComponent({
|
||||
name: 'TransButton',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
noStyle: PropTypes.looseBool,
|
||||
onClick: PropTypes.func,
|
||||
},
|
||||
|
||||
methods: {
|
||||
onKeyDown(event) {
|
||||
const { keyCode } = event;
|
||||
if (keyCode === KeyCode.ENTER) {
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
onKeyUp(event) {
|
||||
const { keyCode } = event;
|
||||
if (keyCode === KeyCode.ENTER) {
|
||||
this.$emit('click', event);
|
||||
}
|
||||
},
|
||||
|
||||
setRef(btn) {
|
||||
this.$refs.div = btn;
|
||||
},
|
||||
|
||||
focus() {
|
||||
if (this.$refs.div) {
|
||||
this.$refs.div.focus();
|
||||
}
|
||||
},
|
||||
|
||||
blur() {
|
||||
if (this.$refs.div) {
|
||||
this.$refs.div.blur();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
render() {
|
||||
const { noStyle, onClick } = this.$props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabindex={0}
|
||||
ref="div"
|
||||
{...this.$attrs}
|
||||
onClick={onClick}
|
||||
onKeydown={this.onKeyDown}
|
||||
onKeyup={this.onKeyUp}
|
||||
style={{ ...(!noStyle ? inlineStyle : null) }}
|
||||
>
|
||||
{this.$slots.default?.()}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default TransButton;
|
|
@ -0,0 +1,101 @@
|
|||
import { defineComponent, CSSProperties, ref, onMounted } from 'vue';
|
||||
/**
|
||||
* Wrap of sub component which need use as Button capacity (like Icon component).
|
||||
* This helps accessibility reader to tread as a interactive button to operation.
|
||||
*/
|
||||
import KeyCode from './KeyCode';
|
||||
import PropTypes from './vue-types';
|
||||
|
||||
const inlineStyle = {
|
||||
border: 0,
|
||||
background: 'transparent',
|
||||
padding: 0,
|
||||
lineHeight: 'inherit',
|
||||
display: 'inline-block',
|
||||
};
|
||||
|
||||
const TransButton = defineComponent({
|
||||
name: 'TransButton',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
noStyle: PropTypes.looseBool,
|
||||
onClick: PropTypes.func,
|
||||
disabled: PropTypes.looseBool,
|
||||
autofocus: PropTypes.looseBool,
|
||||
},
|
||||
setup(props, { slots, emit, attrs, expose }) {
|
||||
const domRef = ref();
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
const { keyCode } = event;
|
||||
if (keyCode === KeyCode.ENTER) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyUp = (event: KeyboardEvent) => {
|
||||
const { keyCode } = event;
|
||||
if (keyCode === KeyCode.ENTER) {
|
||||
emit('click', event);
|
||||
}
|
||||
};
|
||||
const onClick = (e: Event) => {
|
||||
emit('click', e);
|
||||
};
|
||||
const focus = () => {
|
||||
if (domRef.value) {
|
||||
domRef.value.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const blur = () => {
|
||||
if (domRef.value) {
|
||||
domRef.value.blur();
|
||||
}
|
||||
};
|
||||
onMounted(() => {
|
||||
if (props.autofocus) {
|
||||
focus();
|
||||
}
|
||||
});
|
||||
|
||||
expose({
|
||||
focus,
|
||||
blur,
|
||||
});
|
||||
return () => {
|
||||
const { noStyle, disabled, ...restProps } = props;
|
||||
|
||||
let mergedStyle: CSSProperties = {};
|
||||
|
||||
if (!noStyle) {
|
||||
mergedStyle = {
|
||||
...inlineStyle,
|
||||
};
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
mergedStyle.pointerEvents = 'none';
|
||||
}
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabindex={0}
|
||||
ref={domRef}
|
||||
{...restProps}
|
||||
{...attrs}
|
||||
onClick={onClick}
|
||||
onKeydown={onKeyDown}
|
||||
onKeyup={onKeyUp}
|
||||
style={{
|
||||
...mergedStyle,
|
||||
...((attrs.style as object) || {}),
|
||||
}}
|
||||
>
|
||||
{slots.default?.()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default TransButton;
|
|
@ -1,4 +1,12 @@
|
|||
import { defineComponent, nextTick, Transition as T, TransitionGroup as TG } from 'vue';
|
||||
import {
|
||||
BaseTransitionProps,
|
||||
CSSProperties,
|
||||
defineComponent,
|
||||
nextTick,
|
||||
Ref,
|
||||
Transition as T,
|
||||
TransitionGroup as TG,
|
||||
} from 'vue';
|
||||
import { findDOMNode } from './props-util';
|
||||
|
||||
export const getTransitionProps = (transitionName: string, opt: object = {}) => {
|
||||
|
@ -80,6 +88,63 @@ if (process.env.NODE_ENV === 'test') {
|
|||
});
|
||||
}
|
||||
|
||||
export { Transition, TransitionGroup };
|
||||
export declare type MotionEvent = (TransitionEvent | AnimationEvent) & {
|
||||
deadline?: boolean;
|
||||
};
|
||||
|
||||
export declare type MotionEventHandler = (element: Element, done?: () => void) => CSSProperties;
|
||||
|
||||
export declare type MotionEndEventHandler = (element: Element, done?: () => void) => boolean | void;
|
||||
|
||||
// ================== Collapse Motion ==================
|
||||
const getCollapsedHeight: MotionEventHandler = () => ({ height: 0, opacity: 0 });
|
||||
const getRealHeight: MotionEventHandler = node => ({
|
||||
height: `${node.scrollHeight}px`,
|
||||
opacity: 1,
|
||||
});
|
||||
const getCurrentHeight: MotionEventHandler = (node: any) => ({ height: `${node.offsetHeight}px` });
|
||||
// const skipOpacityTransition: MotionEndEventHandler = (_, event) =>
|
||||
// (event as TransitionEvent).propertyName === 'height';
|
||||
|
||||
export interface CSSMotionProps extends Partial<BaseTransitionProps<Element>> {
|
||||
name?: string;
|
||||
css?: boolean;
|
||||
}
|
||||
|
||||
const collapseMotion = (style: Ref<CSSProperties>, className: Ref<string>): CSSMotionProps => {
|
||||
return {
|
||||
name: 'ant-motion-collapse',
|
||||
appear: true,
|
||||
css: true,
|
||||
onBeforeEnter: node => {
|
||||
className.value = 'ant-motion-collapse';
|
||||
style.value = getCollapsedHeight(node);
|
||||
},
|
||||
onEnter: node => {
|
||||
nextTick(() => {
|
||||
style.value = getRealHeight(node);
|
||||
});
|
||||
},
|
||||
onAfterEnter: () => {
|
||||
className.value = '';
|
||||
style.value = {};
|
||||
},
|
||||
onBeforeLeave: node => {
|
||||
className.value = 'ant-motion-collapse';
|
||||
style.value = getCurrentHeight(node);
|
||||
},
|
||||
onLeave: node => {
|
||||
window.setTimeout(() => {
|
||||
style.value = getCollapsedHeight(node);
|
||||
});
|
||||
},
|
||||
onAfterLeave: () => {
|
||||
className.value = '';
|
||||
style.value = {};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { Transition, TransitionGroup, collapseMotion };
|
||||
|
||||
export default Transition;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import {
|
||||
CSSProperties,
|
||||
defineComponent,
|
||||
inject,
|
||||
ref,
|
||||
reactive,
|
||||
watch,
|
||||
|
@ -10,13 +9,13 @@ import {
|
|||
computed,
|
||||
onUnmounted,
|
||||
onUpdated,
|
||||
ExtractPropTypes,
|
||||
} from 'vue';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import classNames from '../_util/classNames';
|
||||
import omit from 'omit.js';
|
||||
import ResizeObserver from '../vc-resize-observer';
|
||||
import throttleByAnimationFrame from '../_util/throttleByAnimationFrame';
|
||||
import { defaultConfigProvider } from '../config-provider';
|
||||
import { withInstall } from '../_util/type';
|
||||
import {
|
||||
addObserveTarget,
|
||||
|
@ -25,6 +24,7 @@ import {
|
|||
getFixedTop,
|
||||
getFixedBottom,
|
||||
} from './utils';
|
||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||
|
||||
function getDefaultTarget() {
|
||||
return typeof window !== 'undefined' ? window : null;
|
||||
|
@ -42,7 +42,7 @@ export interface AffixState {
|
|||
}
|
||||
|
||||
// Affix
|
||||
const AffixProps = {
|
||||
const affixProps = {
|
||||
/**
|
||||
* 距离窗口顶部达到指定偏移量后触发
|
||||
*/
|
||||
|
@ -58,12 +58,14 @@ const AffixProps = {
|
|||
onChange: PropTypes.func,
|
||||
onTestUpdatePosition: PropTypes.func,
|
||||
};
|
||||
|
||||
export type AffixProps = Partial<ExtractPropTypes<typeof affixProps>>;
|
||||
|
||||
const Affix = defineComponent({
|
||||
name: 'AAffix',
|
||||
props: AffixProps,
|
||||
props: affixProps,
|
||||
emits: ['change', 'testUpdatePosition'],
|
||||
setup(props, { slots, emit, expose }) {
|
||||
const configProvider = inject('configProvider', defaultConfigProvider);
|
||||
const placeholderNode = ref();
|
||||
const fixedNode = ref();
|
||||
const state = reactive({
|
||||
|
@ -218,12 +220,12 @@ const Affix = defineComponent({
|
|||
(lazyUpdatePosition as any).cancel();
|
||||
});
|
||||
|
||||
const { prefixCls } = useConfigInject('affix', props);
|
||||
|
||||
return () => {
|
||||
const { prefixCls } = props;
|
||||
const { affixStyle, placeholderStyle } = state;
|
||||
const { getPrefixCls } = configProvider;
|
||||
const className = classNames({
|
||||
[getPrefixCls('affix', prefixCls)]: affixStyle,
|
||||
[prefixCls.value]: affixStyle,
|
||||
});
|
||||
const restProps = omit(props, ['prefixCls', 'offsetTop', 'offsetBottom', 'target']);
|
||||
return (
|
||||
|
|
|
@ -1,23 +1,28 @@
|
|||
import { defineComponent, inject, nextTick, provide } from 'vue';
|
||||
import {
|
||||
defineComponent,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
onUpdated,
|
||||
reactive,
|
||||
ref,
|
||||
ExtractPropTypes,
|
||||
computed,
|
||||
} from 'vue';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import classNames from '../_util/classNames';
|
||||
import addEventListener from '../vc-util/Dom/addEventListener';
|
||||
import Affix from '../affix';
|
||||
import scrollTo from '../_util/scrollTo';
|
||||
import getScroll from '../_util/getScroll';
|
||||
import { findDOMNode } from '../_util/props-util';
|
||||
import BaseMixin from '../_util/BaseMixin';
|
||||
import { defaultConfigProvider } from '../config-provider';
|
||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||
import useProvideAnchor from './context';
|
||||
|
||||
function getDefaultContainer() {
|
||||
return window;
|
||||
}
|
||||
|
||||
function getOffsetTop(element: HTMLElement, container: AnchorContainer): number {
|
||||
if (!element) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!element.getClientRects().length) {
|
||||
return 0;
|
||||
}
|
||||
|
@ -26,7 +31,7 @@ function getOffsetTop(element: HTMLElement, container: AnchorContainer): number
|
|||
|
||||
if (rect.width || rect.height) {
|
||||
if (container === window) {
|
||||
container = element.ownerDocument.documentElement;
|
||||
container = element.ownerDocument!.documentElement!;
|
||||
return rect.top - container.clientTop;
|
||||
}
|
||||
return rect.top - (container as HTMLElement).getBoundingClientRect().top;
|
||||
|
@ -35,7 +40,7 @@ function getOffsetTop(element: HTMLElement, container: AnchorContainer): number
|
|||
return rect.top;
|
||||
}
|
||||
|
||||
const sharpMatcherRegx = /#([^#]+)$/;
|
||||
const sharpMatcherRegx = /#(\S+)$/;
|
||||
|
||||
type Section = {
|
||||
link: string;
|
||||
|
@ -44,7 +49,7 @@ type Section = {
|
|||
|
||||
export type AnchorContainer = HTMLElement | Window;
|
||||
|
||||
const AnchorProps = {
|
||||
const anchorProps = {
|
||||
prefixCls: PropTypes.string,
|
||||
offsetTop: PropTypes.number,
|
||||
bounds: PropTypes.number,
|
||||
|
@ -59,107 +64,40 @@ const AnchorProps = {
|
|||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export interface AntAnchor {
|
||||
registerLink: (link: string) => void;
|
||||
unregisterLink: (link: string) => void;
|
||||
$data: AnchorState;
|
||||
scrollTo: (link: string) => void;
|
||||
$emit?: Function;
|
||||
}
|
||||
export type AnchorProps = Partial<ExtractPropTypes<typeof anchorProps>>;
|
||||
|
||||
export interface AnchorState {
|
||||
activeLink: null | string;
|
||||
scrollContainer: HTMLElement | Window;
|
||||
links: string[];
|
||||
scrollEvent: any;
|
||||
animating: boolean;
|
||||
sPrefixCls?: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AAnchor',
|
||||
mixins: [BaseMixin],
|
||||
inheritAttrs: false,
|
||||
props: AnchorProps,
|
||||
props: anchorProps,
|
||||
emits: ['change', 'click'],
|
||||
setup() {
|
||||
return {
|
||||
configProvider: inject('configProvider', defaultConfigProvider),
|
||||
};
|
||||
},
|
||||
data() {
|
||||
// this.links = [];
|
||||
// this.sPrefixCls = '';
|
||||
return {
|
||||
activeLink: null,
|
||||
setup(props, { emit, attrs, slots, expose }) {
|
||||
const { prefixCls, getTargetContainer, direction } = useConfigInject('anchor', props);
|
||||
const inkNodeRef = ref();
|
||||
const anchorRef = ref();
|
||||
const state = reactive<AnchorState>({
|
||||
links: [],
|
||||
sPrefixCls: '',
|
||||
scrollContainer: null,
|
||||
scrollEvent: null,
|
||||
animating: false,
|
||||
} as AnchorState;
|
||||
},
|
||||
created() {
|
||||
provide('antAnchor', {
|
||||
registerLink: (link: string) => {
|
||||
if (!this.links.includes(link)) {
|
||||
this.links.push(link);
|
||||
}
|
||||
},
|
||||
unregisterLink: (link: string) => {
|
||||
const index = this.links.indexOf(link);
|
||||
if (index !== -1) {
|
||||
this.links.splice(index, 1);
|
||||
}
|
||||
},
|
||||
$data: this.$data,
|
||||
scrollTo: this.handleScrollTo,
|
||||
} as AntAnchor);
|
||||
provide('antAnchorContext', this);
|
||||
},
|
||||
mounted() {
|
||||
nextTick(() => {
|
||||
const { getContainer } = this;
|
||||
this.scrollContainer = getContainer();
|
||||
this.scrollEvent = addEventListener(this.scrollContainer, 'scroll', this.handleScroll);
|
||||
this.handleScroll();
|
||||
});
|
||||
},
|
||||
updated() {
|
||||
nextTick(() => {
|
||||
if (this.scrollEvent) {
|
||||
const { getContainer } = this;
|
||||
const currentContainer = getContainer();
|
||||
if (this.scrollContainer !== currentContainer) {
|
||||
this.scrollContainer = currentContainer;
|
||||
this.scrollEvent.remove();
|
||||
this.scrollEvent = addEventListener(this.scrollContainer, 'scroll', this.handleScroll);
|
||||
this.handleScroll();
|
||||
}
|
||||
}
|
||||
this.updateInk();
|
||||
const activeLink = ref();
|
||||
const getContainer = computed(() => {
|
||||
const { getContainer } = props;
|
||||
return getContainer || getTargetContainer.value || getDefaultContainer;
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.scrollEvent) {
|
||||
this.scrollEvent.remove();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getCurrentActiveLink(offsetTop = 0, bounds = 5) {
|
||||
const { getCurrentAnchor } = this;
|
||||
|
||||
if (typeof getCurrentAnchor === 'function') {
|
||||
return getCurrentAnchor();
|
||||
}
|
||||
const activeLink = '';
|
||||
if (typeof document === 'undefined') {
|
||||
return activeLink;
|
||||
}
|
||||
|
||||
// func...
|
||||
const getCurrentAnchor = (offsetTop = 0, bounds = 5) => {
|
||||
const linkSections: Array<Section> = [];
|
||||
const { getContainer } = this;
|
||||
const container = getContainer();
|
||||
this.links.forEach(link => {
|
||||
const container = getContainer.value();
|
||||
state.links.forEach(link => {
|
||||
const sharpLinkMatch = sharpMatcherRegx.exec(link.toString());
|
||||
if (!sharpLinkMatch) {
|
||||
return;
|
||||
|
@ -181,12 +119,19 @@ export default defineComponent({
|
|||
return maxSection.link;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
};
|
||||
const setCurrentActiveLink = (link: string) => {
|
||||
const { getCurrentAnchor } = props;
|
||||
if (activeLink.value !== link) {
|
||||
return;
|
||||
}
|
||||
activeLink.value = typeof getCurrentAnchor === 'function' ? getCurrentAnchor() : link;
|
||||
emit('change', link);
|
||||
};
|
||||
const handleScrollTo = (link: string) => {
|
||||
const { offsetTop, getContainer, targetOffset } = props;
|
||||
|
||||
handleScrollTo(link: string) {
|
||||
const { offsetTop, getContainer, targetOffset } = this;
|
||||
|
||||
this.setCurrentActiveLink(link);
|
||||
setCurrentActiveLink(link);
|
||||
const container = getContainer();
|
||||
const scrollTop = getScroll(container, true);
|
||||
const sharpLinkMatch = sharpMatcherRegx.exec(link);
|
||||
|
@ -201,99 +146,123 @@ export default defineComponent({
|
|||
const eleOffsetTop = getOffsetTop(targetElement, container);
|
||||
let y = scrollTop + eleOffsetTop;
|
||||
y -= targetOffset !== undefined ? targetOffset : offsetTop || 0;
|
||||
this.animating = true;
|
||||
state.animating = true;
|
||||
|
||||
scrollTo(y, {
|
||||
callback: () => {
|
||||
this.animating = false;
|
||||
state.animating = false;
|
||||
},
|
||||
getContainer,
|
||||
});
|
||||
},
|
||||
setCurrentActiveLink(link: string) {
|
||||
const { activeLink } = this;
|
||||
|
||||
if (activeLink !== link) {
|
||||
this.setState({
|
||||
activeLink: link,
|
||||
});
|
||||
this.$emit('change', link);
|
||||
}
|
||||
},
|
||||
|
||||
handleScroll() {
|
||||
if (this.animating) {
|
||||
};
|
||||
expose({
|
||||
scrollTo: handleScrollTo,
|
||||
});
|
||||
const handleScroll = () => {
|
||||
if (state.animating) {
|
||||
return;
|
||||
}
|
||||
const { offsetTop, bounds, targetOffset } = this;
|
||||
const currentActiveLink = this.getCurrentActiveLink(
|
||||
const { offsetTop, bounds, targetOffset } = props;
|
||||
const currentActiveLink = getCurrentAnchor(
|
||||
targetOffset !== undefined ? targetOffset : offsetTop || 0,
|
||||
bounds,
|
||||
);
|
||||
this.setCurrentActiveLink(currentActiveLink);
|
||||
},
|
||||
setCurrentActiveLink(currentActiveLink);
|
||||
};
|
||||
|
||||
updateInk() {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const { sPrefixCls } = this;
|
||||
const linkNode = findDOMNode(this).getElementsByClassName(
|
||||
`${sPrefixCls}-link-title-active`,
|
||||
const updateInk = () => {
|
||||
const linkNode = anchorRef.value.getElementsByClassName(
|
||||
`${prefixCls.value}-link-title-active`,
|
||||
)[0];
|
||||
if (linkNode) {
|
||||
(this.$refs.inkNode as HTMLElement).style.top = `${linkNode.offsetTop +
|
||||
(inkNodeRef.value as HTMLElement).style.top = `${linkNode.offsetTop +
|
||||
linkNode.clientHeight / 2 -
|
||||
4.5}px`;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
render() {
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
offsetTop,
|
||||
affix,
|
||||
showInkInFixed,
|
||||
activeLink,
|
||||
$slots,
|
||||
getContainer,
|
||||
} = this;
|
||||
const getPrefixCls = this.configProvider.getPrefixCls;
|
||||
const prefixCls = getPrefixCls('anchor', customizePrefixCls);
|
||||
this.sPrefixCls = prefixCls;
|
||||
|
||||
const inkClass = classNames(`${prefixCls}-ink-ball`, {
|
||||
visible: activeLink,
|
||||
});
|
||||
|
||||
const wrapperClass = classNames(this.wrapperClass, `${prefixCls}-wrapper`);
|
||||
|
||||
const anchorClass = classNames(prefixCls, {
|
||||
fixed: !affix && !showInkInFixed,
|
||||
});
|
||||
|
||||
const wrapperStyle = {
|
||||
maxHeight: offsetTop ? `calc(100vh - ${offsetTop}px)` : '100vh',
|
||||
...this.wrapperStyle,
|
||||
};
|
||||
const anchorContent = (
|
||||
<div class={wrapperClass} style={wrapperStyle}>
|
||||
<div class={anchorClass}>
|
||||
<div class={`${prefixCls}-ink`}>
|
||||
<span class={inkClass} ref="inkNode" />
|
||||
</div>
|
||||
{$slots.default?.()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return !affix ? (
|
||||
anchorContent
|
||||
) : (
|
||||
<Affix {...this.$attrs} offsetTop={offsetTop} target={getContainer}>
|
||||
{anchorContent}
|
||||
</Affix>
|
||||
);
|
||||
useProvideAnchor({
|
||||
registerLink: (link: string) => {
|
||||
if (!state.links.includes(link)) {
|
||||
state.links.push(link);
|
||||
}
|
||||
},
|
||||
unregisterLink: (link: string) => {
|
||||
const index = state.links.indexOf(link);
|
||||
if (index !== -1) {
|
||||
state.links.splice(index, 1);
|
||||
}
|
||||
},
|
||||
activeLink,
|
||||
scrollTo: handleScrollTo,
|
||||
handleClick: (e, info) => {
|
||||
emit('click', e, info);
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const container = getContainer.value();
|
||||
state.scrollContainer = container;
|
||||
state.scrollEvent = addEventListener(state.scrollContainer, 'scroll', handleScroll);
|
||||
handleScroll();
|
||||
});
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
if (state.scrollEvent) {
|
||||
state.scrollEvent.remove();
|
||||
}
|
||||
});
|
||||
onUpdated(() => {
|
||||
if (state.scrollEvent) {
|
||||
const currentContainer = getContainer.value();
|
||||
if (state.scrollContainer !== currentContainer) {
|
||||
state.scrollContainer = currentContainer;
|
||||
state.scrollEvent.remove();
|
||||
state.scrollEvent = addEventListener(state.scrollContainer, 'scroll', handleScroll);
|
||||
handleScroll();
|
||||
}
|
||||
}
|
||||
updateInk();
|
||||
});
|
||||
|
||||
return () => {
|
||||
const { offsetTop, affix, showInkInFixed } = props;
|
||||
const pre = prefixCls.value;
|
||||
const inkClass = classNames(`${pre}-ink-ball`, {
|
||||
visible: activeLink.value,
|
||||
});
|
||||
|
||||
const wrapperClass = classNames(props.wrapperClass, `${pre}-wrapper`, {
|
||||
[`${pre}-rtl`]: direction.value === 'rtl',
|
||||
});
|
||||
|
||||
const anchorClass = classNames(pre, {
|
||||
fixed: !affix && !showInkInFixed,
|
||||
});
|
||||
|
||||
const wrapperStyle = {
|
||||
maxHeight: offsetTop ? `calc(100vh - ${offsetTop}px)` : '100vh',
|
||||
...props.wrapperStyle,
|
||||
};
|
||||
const anchorContent = (
|
||||
<div class={wrapperClass} style={wrapperStyle} ref={anchorRef}>
|
||||
<div class={anchorClass}>
|
||||
<div class={`${pre}-ink`}>
|
||||
<span class={inkClass} ref={inkNodeRef} />
|
||||
</div>
|
||||
{slots.default?.()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return !affix ? (
|
||||
anchorContent
|
||||
) : (
|
||||
<Affix {...attrs} offsetTop={offsetTop} target={getContainer.value}>
|
||||
{anchorContent}
|
||||
</Affix>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,89 +1,91 @@
|
|||
import { ComponentPublicInstance, defineComponent, inject, nextTick } from 'vue';
|
||||
import {
|
||||
defineComponent,
|
||||
ExtractPropTypes,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
watch,
|
||||
} from 'vue';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import { getComponent } from '../_util/props-util';
|
||||
import { getPropsSlot } from '../_util/props-util';
|
||||
import classNames from '../_util/classNames';
|
||||
import { defaultConfigProvider } from '../config-provider';
|
||||
import { AntAnchor } from './Anchor';
|
||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||
import { useInjectAnchor } from './context';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function noop(..._any: any[]): any {}
|
||||
|
||||
const AnchorLinkProps = {
|
||||
const anchorLinkProps = {
|
||||
prefixCls: PropTypes.string,
|
||||
href: PropTypes.string.def('#'),
|
||||
title: PropTypes.VNodeChild,
|
||||
target: PropTypes.string,
|
||||
};
|
||||
|
||||
export type AnchorLinkProps = Partial<ExtractPropTypes<typeof anchorLinkProps>>;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AAnchorLink',
|
||||
props: AnchorLinkProps,
|
||||
setup() {
|
||||
return {
|
||||
antAnchor: inject('antAnchor', {
|
||||
registerLink: noop,
|
||||
unregisterLink: noop,
|
||||
scrollTo: noop,
|
||||
$data: {},
|
||||
} as AntAnchor),
|
||||
antAnchorContext: inject('antAnchorContext', {}) as ComponentPublicInstance,
|
||||
configProvider: inject('configProvider', defaultConfigProvider),
|
||||
props: anchorLinkProps,
|
||||
slots: ['title'],
|
||||
setup(props, { slots }) {
|
||||
let mergedTitle = null;
|
||||
const {
|
||||
handleClick: contextHandleClick,
|
||||
scrollTo,
|
||||
unregisterLink,
|
||||
registerLink,
|
||||
activeLink,
|
||||
} = useInjectAnchor();
|
||||
const { prefixCls } = useConfigInject('anchor', props);
|
||||
|
||||
const handleClick = (e: Event) => {
|
||||
const { href } = props;
|
||||
contextHandleClick(e, { title: mergedTitle, href });
|
||||
scrollTo(href);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.href,
|
||||
(val, oldVal) => {
|
||||
nextTick(() => {
|
||||
unregisterLink(oldVal);
|
||||
registerLink(val);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
registerLink(props.href);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unregisterLink(props.href);
|
||||
});
|
||||
|
||||
return () => {
|
||||
const { href, target } = props;
|
||||
const pre = prefixCls.value;
|
||||
const title = getPropsSlot(slots, props, 'title');
|
||||
mergedTitle = title;
|
||||
const active = activeLink.value === href;
|
||||
const wrapperClassName = classNames(`${pre}-link`, {
|
||||
[`${pre}-link-active`]: active,
|
||||
});
|
||||
const titleClassName = classNames(`${pre}-link-title`, {
|
||||
[`${pre}-link-title-active`]: active,
|
||||
});
|
||||
return (
|
||||
<div class={wrapperClassName}>
|
||||
<a
|
||||
class={titleClassName}
|
||||
href={href}
|
||||
title={typeof title === 'string' ? title : ''}
|
||||
target={target}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{title}
|
||||
</a>
|
||||
{slots.default?.()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
href(val, oldVal) {
|
||||
nextTick(() => {
|
||||
this.antAnchor.unregisterLink(oldVal);
|
||||
this.antAnchor.registerLink(val);
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.antAnchor.registerLink(this.href);
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
this.antAnchor.unregisterLink(this.href);
|
||||
},
|
||||
methods: {
|
||||
handleClick(e: Event) {
|
||||
this.antAnchor.scrollTo(this.href);
|
||||
const { scrollTo } = this.antAnchor;
|
||||
const { href, title } = this.$props;
|
||||
if (this.antAnchorContext.$emit) {
|
||||
this.antAnchorContext.$emit('click', e, { title, href });
|
||||
}
|
||||
scrollTo(href);
|
||||
},
|
||||
},
|
||||
render() {
|
||||
const { prefixCls: customizePrefixCls, href, $slots, target } = this;
|
||||
|
||||
const getPrefixCls = this.configProvider.getPrefixCls;
|
||||
const prefixCls = getPrefixCls('anchor', customizePrefixCls);
|
||||
|
||||
const title = getComponent(this, 'title');
|
||||
const active = this.antAnchor.$data.activeLink === href;
|
||||
const wrapperClassName = classNames(`${prefixCls}-link`, {
|
||||
[`${prefixCls}-link-active`]: active,
|
||||
});
|
||||
const titleClassName = classNames(`${prefixCls}-link-title`, {
|
||||
[`${prefixCls}-link-title-active`]: active,
|
||||
});
|
||||
return (
|
||||
<div class={wrapperClassName}>
|
||||
<a
|
||||
class={titleClassName}
|
||||
href={href}
|
||||
title={typeof title === 'string' ? title : ''}
|
||||
target={target}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
{title}
|
||||
</a>
|
||||
{$slots.default?.()}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import * as Vue from 'vue';
|
||||
import { asyncExpect } from '@/tests/utils';
|
||||
import { ref } from 'vue';
|
||||
import Anchor from '..';
|
||||
|
||||
const { Link } = Anchor;
|
||||
|
@ -9,13 +8,20 @@ let idCounter = 0;
|
|||
const getHashUrl = () => `Anchor-API-${idCounter++}`;
|
||||
|
||||
describe('Anchor Render', () => {
|
||||
it('Anchor render perfectly', done => {
|
||||
it('Anchor render perfectly', async done => {
|
||||
const hash = getHashUrl();
|
||||
const anchor = ref(null);
|
||||
const activeLink = ref(null);
|
||||
const wrapper = mount(
|
||||
{
|
||||
render() {
|
||||
return (
|
||||
<Anchor ref="anchor">
|
||||
<Anchor
|
||||
ref={anchor}
|
||||
onChange={current => {
|
||||
activeLink.value = current;
|
||||
}}
|
||||
>
|
||||
<Link href={`#${hash}`} title={hash} />
|
||||
</Anchor>
|
||||
);
|
||||
|
@ -23,22 +29,28 @@ describe('Anchor Render', () => {
|
|||
},
|
||||
{ sync: false },
|
||||
);
|
||||
Vue.nextTick(() => {
|
||||
|
||||
wrapper.vm.$nextTick(() => {
|
||||
wrapper.find(`a[href="#${hash}`).trigger('click');
|
||||
wrapper.vm.$refs.anchor.handleScroll();
|
||||
|
||||
setTimeout(() => {
|
||||
expect(wrapper.vm.$refs.anchor.$data.activeLink).not.toBe(null);
|
||||
expect(activeLink.value).not.toBe(hash);
|
||||
done();
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
|
||||
it('Anchor render perfectly for complete href - click', done => {
|
||||
it('Anchor render perfectly for complete href - click', async done => {
|
||||
const currentActiveLink = ref(null);
|
||||
const wrapper = mount(
|
||||
{
|
||||
render() {
|
||||
return (
|
||||
<Anchor ref="anchor">
|
||||
<Anchor
|
||||
ref="anchor"
|
||||
onChange={current => {
|
||||
currentActiveLink.value = current;
|
||||
}}
|
||||
>
|
||||
<Link href="http://www.example.com/#API" title="API" />
|
||||
</Anchor>
|
||||
);
|
||||
|
@ -46,160 +58,163 @@ describe('Anchor Render', () => {
|
|||
},
|
||||
{ sync: false },
|
||||
);
|
||||
Vue.nextTick(() => {
|
||||
|
||||
wrapper.vm.$nextTick(() => {
|
||||
wrapper.find('a[href="http://www.example.com/#API"]').trigger('click');
|
||||
expect(wrapper.vm.$refs.anchor.$data.activeLink).toBe('http://www.example.com/#API');
|
||||
|
||||
expect(currentActiveLink.value).toBe('http://www.example.com/#API');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Anchor render perfectly for complete href - scroll', done => {
|
||||
const wrapper = mount(
|
||||
{
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div id="API">Hello</div>
|
||||
<Anchor ref="anchor">
|
||||
<Link href="http://www.example.com/#API" title="API" />
|
||||
</Anchor>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{ sync: false, attachTo: 'body' },
|
||||
);
|
||||
Vue.nextTick(() => {
|
||||
wrapper.vm.$refs.anchor.handleScroll();
|
||||
expect(wrapper.vm.$refs.anchor.$data.activeLink).toBe('http://www.example.com/#API');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Anchor render perfectly for complete href - scrollTo', async () => {
|
||||
const scrollToSpy = jest.spyOn(window, 'scrollTo');
|
||||
const wrapper = mount(
|
||||
{
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div id="API">Hello</div>
|
||||
<Anchor ref="anchor">
|
||||
<Link href="##API" title="API" />
|
||||
</Anchor>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{ sync: false, attachTo: 'body' },
|
||||
);
|
||||
await asyncExpect(() => {
|
||||
wrapper.vm.$refs.anchor.handleScrollTo('##API');
|
||||
expect(wrapper.vm.$refs.anchor.$data.activeLink).toBe('##API');
|
||||
expect(scrollToSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
await asyncExpect(() => {
|
||||
expect(scrollToSpy).toHaveBeenCalled();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
it('should remove listener when unmount', async () => {
|
||||
const wrapper = mount(
|
||||
{
|
||||
render() {
|
||||
return (
|
||||
<Anchor ref="anchor">
|
||||
<Link href="#API" title="API" />
|
||||
</Anchor>
|
||||
);
|
||||
},
|
||||
},
|
||||
{ sync: false, attachTo: 'body' },
|
||||
);
|
||||
await asyncExpect(() => {
|
||||
const removeListenerSpy = jest.spyOn(wrapper.vm.$refs.anchor.scrollEvent, 'remove');
|
||||
wrapper.unmount();
|
||||
expect(removeListenerSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should unregister link when unmount children', async () => {
|
||||
const wrapper = mount(
|
||||
{
|
||||
props: {
|
||||
showLink: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
/*
|
||||
it('Anchor render perfectly for complete href - scroll', done => {
|
||||
const wrapper = mount(
|
||||
{
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div id="API">Hello</div>
|
||||
<Anchor ref="anchor">
|
||||
<Link href="http://www.example.com/#API" title="API" />
|
||||
</Anchor>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<Anchor ref="anchor">{this.showLink ? <Link href="#API" title="API" /> : null}</Anchor>
|
||||
);
|
||||
},
|
||||
},
|
||||
{ sync: false, attachTo: 'body' },
|
||||
);
|
||||
await asyncExpect(() => {
|
||||
expect(wrapper.vm.$refs.anchor.links).toEqual(['#API']);
|
||||
wrapper.setProps({ showLink: false });
|
||||
{ sync: false, attachTo: 'body' },
|
||||
);
|
||||
wrapper.vm.$nextTick(() => {
|
||||
wrapper.vm.$refs.anchor.handleScroll();
|
||||
expect(wrapper.vm.$refs.anchor.$data.activeLink).toBe('http://www.example.com/#API');
|
||||
done();
|
||||
});
|
||||
});
|
||||
await asyncExpect(() => {
|
||||
expect(wrapper.vm.$refs.anchor.links).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should update links when link href update', async () => {
|
||||
const wrapper = mount(
|
||||
{
|
||||
props: ['href'],
|
||||
render() {
|
||||
return (
|
||||
<Anchor ref="anchor">
|
||||
<Link href={this.href} title="API" />
|
||||
</Anchor>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
sync: false,
|
||||
attachTo: 'body',
|
||||
props: {
|
||||
href: '#API',
|
||||
},
|
||||
},
|
||||
);
|
||||
await asyncExpect(() => {
|
||||
expect(wrapper.vm.$refs.anchor.links).toEqual(['#API']);
|
||||
wrapper.setProps({ href: '#API_1' });
|
||||
});
|
||||
await asyncExpect(() => {
|
||||
expect(wrapper.vm.$refs.anchor.links).toEqual(['#API_1']);
|
||||
});
|
||||
});
|
||||
|
||||
it('Anchor onClick event', () => {
|
||||
let event;
|
||||
let link;
|
||||
const handleClick = (...arg) => ([event, link] = arg);
|
||||
|
||||
const href = '#API';
|
||||
const title = 'API';
|
||||
|
||||
const wrapper = mount({
|
||||
render() {
|
||||
return (
|
||||
<Anchor ref="anchorRef" onClick={handleClick}>
|
||||
<Link href={href} title={title} />
|
||||
</Anchor>
|
||||
it('Anchor render perfectly for complete href - scrollTo', async () => {
|
||||
const scrollToSpy = jest.spyOn(window, 'scrollTo');
|
||||
const wrapper = mount(
|
||||
{
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div id="API">Hello</div>
|
||||
<Anchor ref="anchor">
|
||||
<Link href="##API" title="API" />
|
||||
</Anchor>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{ sync: false, attachTo: 'body' },
|
||||
);
|
||||
},
|
||||
});
|
||||
await asyncExpect(() => {
|
||||
wrapper.vm.$refs.anchor.handleScrollTo('##API');
|
||||
expect(wrapper.vm.$refs.anchor.$data.activeLink).toBe('##API');
|
||||
expect(scrollToSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
await asyncExpect(() => {
|
||||
expect(scrollToSpy).toHaveBeenCalled();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
wrapper.find(`a[href="${href}"]`).trigger('click');
|
||||
it('should remove listener when unmount', async () => {
|
||||
const wrapper = mount(
|
||||
{
|
||||
render() {
|
||||
return (
|
||||
<Anchor ref="anchor">
|
||||
<Link href="#API" title="API" />
|
||||
</Anchor>
|
||||
);
|
||||
},
|
||||
},
|
||||
{ sync: false, attachTo: 'body' },
|
||||
);
|
||||
await asyncExpect(() => {
|
||||
const removeListenerSpy = jest.spyOn(wrapper.vm.$refs.anchor.scrollEvent, 'remove');
|
||||
wrapper.unmount();
|
||||
expect(removeListenerSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
wrapper.vm.$refs.anchorRef.handleScroll();
|
||||
expect(event).not.toBe(undefined);
|
||||
expect(link).toEqual({ href, title });
|
||||
});
|
||||
it('should unregister link when unmount children', async () => {
|
||||
const wrapper = mount(
|
||||
{
|
||||
props: {
|
||||
showLink: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<Anchor ref="anchor">{this.showLink ? <Link href="#API" title="API" /> : null}</Anchor>
|
||||
);
|
||||
},
|
||||
},
|
||||
{ sync: false, attachTo: 'body' },
|
||||
);
|
||||
await asyncExpect(() => {
|
||||
expect(wrapper.vm.$refs.anchor.links).toEqual(['#API']);
|
||||
wrapper.setProps({ showLink: false });
|
||||
});
|
||||
await asyncExpect(() => {
|
||||
expect(wrapper.vm.$refs.anchor.links).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should update links when link href update', async () => {
|
||||
const wrapper = mount(
|
||||
{
|
||||
props: ['href'],
|
||||
render() {
|
||||
return (
|
||||
<Anchor ref="anchor">
|
||||
<Link href={this.href} title="API" />
|
||||
</Anchor>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
sync: false,
|
||||
attachTo: 'body',
|
||||
props: {
|
||||
href: '#API',
|
||||
},
|
||||
},
|
||||
);
|
||||
await asyncExpect(() => {
|
||||
expect(wrapper.vm.$refs.anchor.links).toEqual(['#API']);
|
||||
wrapper.setProps({ href: '#API_1' });
|
||||
});
|
||||
await asyncExpect(() => {
|
||||
expect(wrapper.vm.$refs.anchor.links).toEqual(['#API_1']);
|
||||
});
|
||||
});
|
||||
|
||||
it('Anchor onClick event', () => {
|
||||
let event;
|
||||
let link;
|
||||
const handleClick = (...arg) => ([event, link] = arg);
|
||||
|
||||
const href = '#API';
|
||||
const title = 'API';
|
||||
|
||||
const anchorRef = Vue.ref(null);
|
||||
|
||||
const wrapper = mount({
|
||||
render() {
|
||||
return (
|
||||
<Anchor ref={anchorRef} onClick={handleClick}>
|
||||
<Link href={href} title={title} />
|
||||
</Anchor>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.find(`a[href="${href}"]`).trigger('click');
|
||||
anchorRef.value.handleScroll();
|
||||
expect(event).not.toBe(undefined);
|
||||
expect(link).toEqual({ href, title });
|
||||
}); */
|
||||
});
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import { computed, Ref, inject, InjectionKey, provide } from 'vue';
|
||||
|
||||
export interface AnchorContext {
|
||||
registerLink: (link: string) => void;
|
||||
unregisterLink: (link: string) => void;
|
||||
activeLink: Ref<string>;
|
||||
scrollTo: (link: string) => void;
|
||||
handleClick: (e: Event, info: { title: any; href: string }) => void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function noop(..._any: any[]): any {}
|
||||
|
||||
export const AnchorContextKey: InjectionKey<AnchorContext> = Symbol('anchorContextKey');
|
||||
|
||||
const useProvideAnchor = (state: AnchorContext) => {
|
||||
provide(AnchorContextKey, state);
|
||||
};
|
||||
|
||||
const useInjectAnchor = () => {
|
||||
return inject(AnchorContextKey, {
|
||||
registerLink: noop,
|
||||
unregisterLink: noop,
|
||||
scrollTo: noop,
|
||||
activeLink: computed(() => ''),
|
||||
handleClick: noop,
|
||||
} as AnchorContext);
|
||||
};
|
||||
|
||||
export { useInjectAnchor, useProvideAnchor };
|
||||
export default useProvideAnchor;
|
|
@ -1,6 +1,6 @@
|
|||
import { App, Plugin } from 'vue';
|
||||
import Anchor from './Anchor';
|
||||
import AnchorLink from './AnchorLink';
|
||||
import Anchor, { AnchorProps } from './Anchor';
|
||||
import AnchorLink, { AnchorLinkProps } from './AnchorLink';
|
||||
|
||||
Anchor.Link = AnchorLink;
|
||||
|
||||
|
@ -11,6 +11,8 @@ Anchor.install = function(app: App) {
|
|||
return app;
|
||||
};
|
||||
|
||||
export { AnchorLinkProps, AnchorProps, AnchorLink, AnchorLink as Link };
|
||||
|
||||
export default Anchor as typeof Anchor &
|
||||
Plugin & {
|
||||
readonly Link: typeof AnchorLink;
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
margin-left: -4px;
|
||||
padding-left: 4px;
|
||||
overflow: auto;
|
||||
background-color: @component-background;
|
||||
background-color: @anchor-bg;
|
||||
}
|
||||
|
||||
&-ink {
|
||||
|
@ -52,7 +52,7 @@
|
|||
}
|
||||
|
||||
&-link {
|
||||
padding: 7px 0 7px 16px;
|
||||
padding: @anchor-link-padding;
|
||||
line-height: 1.143;
|
||||
|
||||
&-title {
|
||||
|
@ -80,3 +80,5 @@
|
|||
padding-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@import './rtl';
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
.@{ant-prefix}-anchor {
|
||||
&-rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
&-wrapper {
|
||||
.@{ant-prefix}-anchor-rtl& {
|
||||
margin-right: -4px;
|
||||
margin-left: 0;
|
||||
padding-right: 4px;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-ink {
|
||||
.@{ant-prefix}-anchor-rtl & {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
&-ball {
|
||||
.@{ant-prefix}-anchor-rtl & {
|
||||
right: 50%;
|
||||
left: 0;
|
||||
transform: translateX(50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-link {
|
||||
.@{ant-prefix}-anchor-rtl & {
|
||||
padding: @anchor-link-top @anchor-link-left @anchor-link-top 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,134 +1,177 @@
|
|||
import { tuple, VueNode } from '../_util/type';
|
||||
import { CSSProperties, defineComponent, inject, nextTick, PropType } from 'vue';
|
||||
import { defaultConfigProvider } from '../config-provider';
|
||||
import { getComponent } from '../_util/props-util';
|
||||
import {
|
||||
computed,
|
||||
CSSProperties,
|
||||
defineComponent,
|
||||
ExtractPropTypes,
|
||||
nextTick,
|
||||
onMounted,
|
||||
PropType,
|
||||
ref,
|
||||
watch,
|
||||
} from 'vue';
|
||||
import { getPropsSlot } from '../_util/props-util';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import useBreakpoint from '../_util/hooks/useBreakpoint';
|
||||
import { Breakpoint, responsiveArray, ScreenSizeMap } from '../_util/responsiveObserve';
|
||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||
import ResizeObserver from '../vc-resize-observer';
|
||||
import { useInjectSize } from '../_util/hooks/useSize';
|
||||
|
||||
export default defineComponent({
|
||||
export type AvatarSize = 'large' | 'small' | 'default' | number | ScreenSizeMap;
|
||||
|
||||
export const avatarProps = {
|
||||
prefixCls: PropTypes.string,
|
||||
shape: PropTypes.oneOf(tuple('circle', 'square')).def('circle'),
|
||||
size: {
|
||||
type: [Number, String, Object] as PropType<AvatarSize>,
|
||||
default: (): AvatarSize => 'default',
|
||||
},
|
||||
src: PropTypes.string,
|
||||
/** Srcset of image avatar */
|
||||
srcset: PropTypes.string,
|
||||
icon: PropTypes.VNodeChild,
|
||||
alt: PropTypes.string,
|
||||
gap: PropTypes.number,
|
||||
draggable: PropTypes.bool,
|
||||
loadError: {
|
||||
type: Function as PropType<() => boolean>,
|
||||
},
|
||||
};
|
||||
|
||||
export type AvatarProps = Partial<ExtractPropTypes<typeof avatarProps>>;
|
||||
|
||||
const Avatar = defineComponent({
|
||||
name: 'AAvatar',
|
||||
props: {
|
||||
prefixCls: PropTypes.string,
|
||||
shape: PropTypes.oneOf(tuple('circle', 'square')),
|
||||
size: {
|
||||
type: [Number, String] as PropType<'large' | 'small' | 'default' | number>,
|
||||
default: 'default',
|
||||
},
|
||||
src: PropTypes.string,
|
||||
/** Srcset of image avatar */
|
||||
srcset: PropTypes.string,
|
||||
/** @deprecated please use `srcset` instead `srcSet` */
|
||||
srcSet: PropTypes.string,
|
||||
icon: PropTypes.VNodeChild,
|
||||
alt: PropTypes.string,
|
||||
loadError: {
|
||||
type: Function as PropType<() => boolean>,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
configProvider: inject('configProvider', defaultConfigProvider),
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isImgExist: true,
|
||||
isMounted: false,
|
||||
scale: 1,
|
||||
lastChildrenWidth: undefined,
|
||||
lastNodeWidth: undefined,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
src() {
|
||||
nextTick(() => {
|
||||
this.isImgExist = true;
|
||||
this.scale = 1;
|
||||
// force uodate for position
|
||||
this.$forceUpdate();
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
nextTick(() => {
|
||||
this.setScale();
|
||||
this.isMounted = true;
|
||||
inheritAttrs: false,
|
||||
props: avatarProps,
|
||||
slots: ['icon'],
|
||||
setup(props, { slots, attrs }) {
|
||||
const isImgExist = ref(true);
|
||||
const isMounted = ref(false);
|
||||
const scale = ref(1);
|
||||
|
||||
const avatarChildrenRef = ref<HTMLElement>(null);
|
||||
const avatarNodeRef = ref<HTMLElement>(null);
|
||||
|
||||
const { prefixCls } = useConfigInject('avatar', props);
|
||||
|
||||
const groupSize = useInjectSize();
|
||||
|
||||
const screens = useBreakpoint();
|
||||
const responsiveSize = computed(() => {
|
||||
if (typeof props.size !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
const currentBreakpoint: Breakpoint = responsiveArray.find(screen => screens.value[screen])!;
|
||||
const currentSize = props.size[currentBreakpoint];
|
||||
|
||||
return currentSize;
|
||||
});
|
||||
},
|
||||
updated() {
|
||||
nextTick(() => {
|
||||
this.setScale();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
setScale() {
|
||||
if (!this.$refs.avatarChildren || !this.$refs.avatarNode) {
|
||||
|
||||
const responsiveSizeStyle = (hasIcon: boolean) => {
|
||||
if (responsiveSize.value) {
|
||||
return {
|
||||
width: `${responsiveSize.value}px`,
|
||||
height: `${responsiveSize.value}px`,
|
||||
lineHeight: `${responsiveSize.value}px`,
|
||||
fontSize: `${hasIcon ? responsiveSize.value / 2 : 18}px`,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const setScaleParam = () => {
|
||||
if (!avatarChildrenRef.value || !avatarNodeRef.value) {
|
||||
return;
|
||||
}
|
||||
const childrenWidth = (this.$refs.avatarChildren as HTMLElement).offsetWidth; // offsetWidth avoid affecting be transform scale
|
||||
const nodeWidth = (this.$refs.avatarNode as HTMLElement).offsetWidth;
|
||||
const childrenWidth = avatarChildrenRef.value.offsetWidth; // offsetWidth avoid affecting be transform scale
|
||||
const nodeWidth = avatarNodeRef.value.offsetWidth;
|
||||
// denominator is 0 is no meaning
|
||||
if (
|
||||
childrenWidth === 0 ||
|
||||
nodeWidth === 0 ||
|
||||
(this.lastChildrenWidth === childrenWidth && this.lastNodeWidth === nodeWidth)
|
||||
) {
|
||||
return;
|
||||
if (childrenWidth !== 0 && nodeWidth !== 0) {
|
||||
const { gap = 4 } = props;
|
||||
if (gap * 2 < nodeWidth) {
|
||||
scale.value =
|
||||
nodeWidth - gap * 2 < childrenWidth ? (nodeWidth - gap * 2) / childrenWidth : 1;
|
||||
}
|
||||
}
|
||||
this.lastChildrenWidth = childrenWidth;
|
||||
this.lastNodeWidth = nodeWidth;
|
||||
// add 4px gap for each side to get better performance
|
||||
this.scale = nodeWidth - 8 < childrenWidth ? (nodeWidth - 8) / childrenWidth : 1;
|
||||
},
|
||||
handleImgLoadError() {
|
||||
const { loadError } = this.$props;
|
||||
const errorFlag = loadError ? loadError() : undefined;
|
||||
};
|
||||
|
||||
const handleImgLoadError = () => {
|
||||
const { loadError } = props;
|
||||
const errorFlag = loadError?.();
|
||||
if (errorFlag !== false) {
|
||||
this.isImgExist = false;
|
||||
isImgExist.value = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
render() {
|
||||
const { prefixCls: customizePrefixCls, shape, size, src, alt, srcset, srcSet } = this.$props;
|
||||
const icon = getComponent(this, 'icon');
|
||||
const getPrefixCls = this.configProvider.getPrefixCls;
|
||||
const prefixCls = getPrefixCls('avatar', customizePrefixCls);
|
||||
|
||||
const { isImgExist, scale, isMounted } = this.$data;
|
||||
|
||||
const sizeCls = {
|
||||
[`${prefixCls}-lg`]: size === 'large',
|
||||
[`${prefixCls}-sm`]: size === 'small',
|
||||
};
|
||||
|
||||
const classString = {
|
||||
[prefixCls]: true,
|
||||
...sizeCls,
|
||||
[`${prefixCls}-${shape}`]: shape,
|
||||
[`${prefixCls}-image`]: src && isImgExist,
|
||||
[`${prefixCls}-icon`]: icon,
|
||||
};
|
||||
watch(
|
||||
() => props.src,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
isImgExist.value = true;
|
||||
scale.value = 1;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const sizeStyle: CSSProperties =
|
||||
typeof size === 'number'
|
||||
? {
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
lineHeight: `${size}px`,
|
||||
fontSize: icon ? `${size / 2}px` : '18px',
|
||||
}
|
||||
: {};
|
||||
watch(
|
||||
() => props.gap,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
setScaleParam();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let children: VueNode = this.$slots.default?.();
|
||||
if (src && isImgExist) {
|
||||
children = (
|
||||
<img src={src} srcset={srcset || srcSet} onError={this.handleImgLoadError} alt={alt} />
|
||||
);
|
||||
} else if (icon) {
|
||||
children = icon;
|
||||
} else {
|
||||
const childrenNode = this.$refs.avatarChildren;
|
||||
if (childrenNode || scale !== 1) {
|
||||
const transformString = `scale(${scale}) translateX(-50%)`;
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
setScaleParam();
|
||||
isMounted.value = true;
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
const { shape, size: customSize, src, alt, srcset, draggable } = props;
|
||||
const icon = getPropsSlot(slots, props, 'icon');
|
||||
const pre = prefixCls.value;
|
||||
const size = customSize === 'default' ? groupSize.value : customSize;
|
||||
const classString = {
|
||||
[`${attrs.class}`]: !!attrs.class,
|
||||
[pre]: true,
|
||||
[`${pre}-lg`]: size === 'large',
|
||||
[`${pre}-sm`]: size === 'small',
|
||||
[`${pre}-${shape}`]: shape,
|
||||
[`${pre}-image`]: src && isImgExist.value,
|
||||
[`${pre}-icon`]: icon,
|
||||
};
|
||||
|
||||
const sizeStyle: CSSProperties =
|
||||
typeof size === 'number'
|
||||
? {
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
lineHeight: `${size}px`,
|
||||
fontSize: icon ? `${size / 2}px` : '18px',
|
||||
}
|
||||
: {};
|
||||
|
||||
const children: VueNode = slots.default?.();
|
||||
let childrenToRender;
|
||||
if (src && isImgExist.value) {
|
||||
childrenToRender = (
|
||||
<img
|
||||
draggable={draggable}
|
||||
src={src}
|
||||
srcset={srcset}
|
||||
onError={handleImgLoadError}
|
||||
alt={alt}
|
||||
/>
|
||||
);
|
||||
} else if (icon) {
|
||||
childrenToRender = icon;
|
||||
} else if (isMounted.value || scale.value !== 1) {
|
||||
const transformString = `scale(${scale.value}) translateX(-50%)`;
|
||||
const childrenStyle: CSSProperties = {
|
||||
msTransform: transformString,
|
||||
WebkitTransform: transformString,
|
||||
|
@ -140,31 +183,40 @@ export default defineComponent({
|
|||
lineHeight: `${size}px`,
|
||||
}
|
||||
: {};
|
||||
children = (
|
||||
<span
|
||||
class={`${prefixCls}-string`}
|
||||
ref="avatarChildren"
|
||||
style={{ ...sizeChildrenStyle, ...childrenStyle }}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
childrenToRender = (
|
||||
<ResizeObserver onResize={setScaleParam}>
|
||||
<span
|
||||
class={`${pre}-string`}
|
||||
ref={avatarChildrenRef}
|
||||
style={{ ...sizeChildrenStyle, ...childrenStyle }}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
</ResizeObserver>
|
||||
);
|
||||
} else {
|
||||
const childrenStyle: CSSProperties = {};
|
||||
if (!isMounted) {
|
||||
childrenStyle.opacity = 0;
|
||||
}
|
||||
children = (
|
||||
<span class={`${prefixCls}-string`} ref="avatarChildren" style={{ opacity: 0 }}>
|
||||
childrenToRender = (
|
||||
<span class={`${pre}-string`} ref={avatarChildrenRef} style={{ opacity: 0 }}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<span ref="avatarNode" class={classString} style={sizeStyle}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
return (
|
||||
<span
|
||||
{...attrs}
|
||||
ref={avatarNodeRef}
|
||||
class={classString}
|
||||
style={{
|
||||
...sizeStyle,
|
||||
...responsiveSizeStyle(!!icon),
|
||||
...(attrs.style as CSSProperties),
|
||||
}}
|
||||
>
|
||||
{childrenToRender}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default Avatar;
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
import { cloneElement } from '../_util/vnode';
|
||||
import Avatar, { avatarProps, AvatarSize } from './Avatar';
|
||||
import Popover from '../popover';
|
||||
import { defineComponent, PropType, ExtractPropTypes, CSSProperties } from 'vue';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import { flattenChildren, getPropsSlot } from '../_util/props-util';
|
||||
import { tuple } from '../_util/type';
|
||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||
import useProvideSize from '../_util/hooks/useSize';
|
||||
|
||||
const groupProps = {
|
||||
prefixCls: PropTypes.string,
|
||||
maxCount: PropTypes.number,
|
||||
maxStyle: {
|
||||
type: Object as PropType<CSSProperties>,
|
||||
default: () => ({} as CSSProperties),
|
||||
},
|
||||
maxPopoverPlacement: PropTypes.oneOf(tuple('top', 'bottom')).def('top'),
|
||||
/*
|
||||
* Size of avatar, options: `large`, `small`, `default`
|
||||
* or a custom number size
|
||||
* */
|
||||
size: avatarProps.size,
|
||||
};
|
||||
|
||||
export type AvatarGroupProps = Partial<ExtractPropTypes<typeof groupProps>> & {
|
||||
size?: AvatarSize;
|
||||
};
|
||||
|
||||
const Group = defineComponent({
|
||||
name: 'AAvatarGroup',
|
||||
inheritAttrs: false,
|
||||
props: groupProps,
|
||||
setup(props, { slots, attrs }) {
|
||||
const { prefixCls, direction } = useConfigInject('avatar-group', props);
|
||||
useProvideSize<AvatarSize>(props);
|
||||
return () => {
|
||||
const { maxPopoverPlacement = 'top', maxCount, maxStyle } = props;
|
||||
|
||||
const cls = {
|
||||
[prefixCls.value]: true,
|
||||
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
|
||||
[`${attrs.class}`]: !!attrs.class,
|
||||
};
|
||||
|
||||
const children = getPropsSlot(slots, props);
|
||||
const childrenWithProps = flattenChildren(children).map((child, index) =>
|
||||
cloneElement(child, {
|
||||
key: `avatar-key-${index}`,
|
||||
}),
|
||||
);
|
||||
|
||||
const numOfChildren = childrenWithProps.length;
|
||||
if (maxCount && maxCount < numOfChildren) {
|
||||
const childrenShow = childrenWithProps.slice(0, maxCount);
|
||||
const childrenHidden = childrenWithProps.slice(maxCount, numOfChildren);
|
||||
|
||||
childrenShow.push(
|
||||
<Popover
|
||||
key="avatar-popover-key"
|
||||
content={childrenHidden}
|
||||
trigger="hover"
|
||||
placement={maxPopoverPlacement}
|
||||
overlayClassName={`${prefixCls.value}-popover`}
|
||||
>
|
||||
<Avatar style={maxStyle}>{`+${numOfChildren - maxCount}`}</Avatar>
|
||||
</Popover>,
|
||||
);
|
||||
return (
|
||||
<div {...attrs} class={cls} style={attrs.style}>
|
||||
{childrenShow}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div {...attrs} class={cls} style={attrs.style}>
|
||||
{childrenWithProps}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default Group;
|
|
@ -1,9 +1,14 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import { asyncExpect } from '@/tests/utils';
|
||||
import Avatar from '..';
|
||||
import useBreakpoint from '../../_util/hooks/useBreakpoint';
|
||||
|
||||
jest.mock('../../_util/hooks/useBreakpoint');
|
||||
|
||||
describe('Avatar Render', () => {
|
||||
let originOffsetWidth;
|
||||
const sizes = { xs: 24, sm: 32, md: 40, lg: 64, xl: 80, xxl: 100 };
|
||||
|
||||
beforeAll(() => {
|
||||
// Mock offsetHeight
|
||||
originOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetWidth').get;
|
||||
|
@ -41,16 +46,8 @@ describe('Avatar Render', () => {
|
|||
props: {
|
||||
src: 'http://error.url',
|
||||
},
|
||||
sync: false,
|
||||
attachTo: 'body',
|
||||
});
|
||||
wrapper.vm.setScale = jest.fn(() => {
|
||||
if (wrapper.vm.scale === 0.5) {
|
||||
return;
|
||||
}
|
||||
wrapper.vm.scale = 0.5;
|
||||
wrapper.vm.$forceUpdate();
|
||||
});
|
||||
await asyncExpect(() => {
|
||||
wrapper.find('img').trigger('error');
|
||||
}, 0);
|
||||
|
@ -58,14 +55,7 @@ describe('Avatar Render', () => {
|
|||
const children = wrapper.findAll('.ant-avatar-string');
|
||||
expect(children.length).toBe(1);
|
||||
expect(children[0].text()).toBe('Fallback');
|
||||
expect(wrapper.vm.setScale).toHaveBeenCalled();
|
||||
});
|
||||
await asyncExpect(() => {
|
||||
expect(global.document.body.querySelector('.ant-avatar-string').style.transform).toContain(
|
||||
'scale(0.5)',
|
||||
);
|
||||
global.document.body.innerHTML = '';
|
||||
}, 1000);
|
||||
});
|
||||
it('should handle onError correctly', async () => {
|
||||
global.document.body.innerHTML = '';
|
||||
|
@ -91,17 +81,17 @@ describe('Avatar Render', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const wrapper = mount(Foo, { sync: false, attachTo: 'body' });
|
||||
const wrapper = mount(Foo, { attachTo: 'body' });
|
||||
await asyncExpect(() => {
|
||||
// mock img load Error, since jsdom do not load resource by default
|
||||
// https://github.com/jsdom/jsdom/issues/1816
|
||||
wrapper.find('img').trigger('error');
|
||||
}, 0);
|
||||
await asyncExpect(() => {
|
||||
expect(wrapper.findComponent({ name: 'AAvatar' }).vm.isImgExist).toBe(true);
|
||||
expect(wrapper.find('img')).not.toBeNull();
|
||||
}, 0);
|
||||
await asyncExpect(() => {
|
||||
expect(global.document.body.querySelector('img').getAttribute('src')).toBe(LOAD_SUCCESS_SRC);
|
||||
expect(wrapper.find('img').attributes('src')).toBe(LOAD_SUCCESS_SRC);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
|
@ -126,9 +116,8 @@ describe('Avatar Render', () => {
|
|||
await asyncExpect(() => {
|
||||
wrapper.find('img').trigger('error');
|
||||
}, 0);
|
||||
|
||||
await asyncExpect(() => {
|
||||
expect(wrapper.findComponent({ name: 'AAvatar' }).vm.isImgExist).toBe(false);
|
||||
expect(wrapper.findComponent({ name: 'AAvatar' }).findAll('img').length).toBe(0);
|
||||
expect(wrapper.findAll('.ant-avatar-string').length).toBe(1);
|
||||
}, 0);
|
||||
|
||||
|
@ -136,8 +125,87 @@ describe('Avatar Render', () => {
|
|||
wrapper.vm.src = LOAD_SUCCESS_SRC;
|
||||
});
|
||||
await asyncExpect(() => {
|
||||
expect(wrapper.findComponent({ name: 'AAvatar' }).vm.isImgExist).toBe(true);
|
||||
expect(wrapper.findComponent({ name: 'AAvatar' }).findAll('img').length).toBe(1);
|
||||
expect(wrapper.findAll('.ant-avatar-image').length).toBe(1);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('should calculate scale of avatar children correctly', async () => {
|
||||
let wrapper = mount({
|
||||
render() {
|
||||
return <Avatar>Avatar</Avatar>;
|
||||
},
|
||||
});
|
||||
|
||||
await asyncExpect(() => {
|
||||
expect(wrapper.find('.ant-avatar-string')).toMatchSnapshot();
|
||||
}, 0);
|
||||
|
||||
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
|
||||
get() {
|
||||
if (this.className === 'ant-avatar-string') {
|
||||
return 100;
|
||||
}
|
||||
return 40;
|
||||
},
|
||||
});
|
||||
wrapper = mount({
|
||||
render() {
|
||||
return <Avatar>xx</Avatar>;
|
||||
},
|
||||
});
|
||||
await asyncExpect(() => {
|
||||
expect(wrapper.find('.ant-avatar-string')).toMatchSnapshot();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('should calculate scale of avatar children correctly with gap', async () => {
|
||||
const wrapper = mount({
|
||||
render() {
|
||||
return <Avatar gap={2}>Avatar</Avatar>;
|
||||
},
|
||||
});
|
||||
await asyncExpect(() => {
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
Object.entries(sizes).forEach(([key, value]) => {
|
||||
it(`adjusts component size to ${value} when window size is ${key}`, async () => {
|
||||
useBreakpoint.mockReturnValue({ value: { [key]: true } });
|
||||
|
||||
const wrapper = mount({
|
||||
render() {
|
||||
return <Avatar size={sizes} />;
|
||||
},
|
||||
});
|
||||
|
||||
await asyncExpect(() => {
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
|
||||
it('fallback', async () => {
|
||||
const div = global.document.createElement('div');
|
||||
global.document.body.appendChild(div);
|
||||
const wrapper = mount(
|
||||
{
|
||||
render() {
|
||||
return (
|
||||
<Avatar shape="circle" src="http://error.url">
|
||||
A
|
||||
</Avatar>
|
||||
);
|
||||
},
|
||||
},
|
||||
{ attachTo: div },
|
||||
);
|
||||
await asyncExpect(async () => {
|
||||
await wrapper.find('img').trigger('error');
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
wrapper.unmount();
|
||||
global.document.body.removeChild(div);
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Avatar Render adjusts component size to 24 when window size is xs 1`] = `<span class="ant-avatar" style="width: 24px; height: 24px; line-height: 24px; font-size: 18px;"><span class="ant-avatar-string" style="transform: scale(0.32) translateX(-50%);"><!----></span></span>`;
|
||||
|
||||
exports[`Avatar Render adjusts component size to 32 when window size is sm 1`] = `<span class="ant-avatar" style="width: 32px; height: 32px; line-height: 32px; font-size: 18px;"><span class="ant-avatar-string" style="transform: scale(0.32) translateX(-50%);"><!----></span></span>`;
|
||||
|
||||
exports[`Avatar Render adjusts component size to 40 when window size is md 1`] = `<span class="ant-avatar" style="width: 40px; height: 40px; line-height: 40px; font-size: 18px;"><span class="ant-avatar-string" style="transform: scale(0.32) translateX(-50%);"><!----></span></span>`;
|
||||
|
||||
exports[`Avatar Render adjusts component size to 64 when window size is lg 1`] = `<span class="ant-avatar" style="width: 64px; height: 64px; line-height: 64px; font-size: 18px;"><span class="ant-avatar-string" style="transform: scale(0.32) translateX(-50%);"><!----></span></span>`;
|
||||
|
||||
exports[`Avatar Render adjusts component size to 80 when window size is xl 1`] = `<span class="ant-avatar" style="width: 80px; height: 80px; line-height: 80px; font-size: 18px;"><span class="ant-avatar-string" style="transform: scale(0.32) translateX(-50%);"><!----></span></span>`;
|
||||
|
||||
exports[`Avatar Render adjusts component size to 100 when window size is xxl 1`] = `<span class="ant-avatar" style="width: 100px; height: 100px; line-height: 100px; font-size: 18px;"><span class="ant-avatar-string" style="transform: scale(0.32) translateX(-50%);"><!----></span></span>`;
|
||||
|
||||
exports[`Avatar Render fallback 1`] = `<span class="ant-avatar ant-avatar-circle"><span class="ant-avatar-string" style="transform: scale(0.32) translateX(-50%);">A</span></span>`;
|
||||
|
||||
exports[`Avatar Render should calculate scale of avatar children correctly 1`] = `
|
||||
DOMWrapper {
|
||||
"wrapperElement": <span
|
||||
class="ant-avatar-string"
|
||||
style="transform: scale(0.72) translateX(-50%);"
|
||||
>
|
||||
|
||||
Avatar
|
||||
|
||||
</span>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Avatar Render should calculate scale of avatar children correctly 2`] = `
|
||||
DOMWrapper {
|
||||
"wrapperElement": <span
|
||||
class="ant-avatar-string"
|
||||
style="transform: scale(0.32) translateX(-50%);"
|
||||
>
|
||||
|
||||
xx
|
||||
|
||||
</span>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Avatar Render should calculate scale of avatar children correctly with gap 1`] = `<span class="ant-avatar"><span class="ant-avatar-string" style="transform: scale(0.36) translateX(-50%);">Avatar</span></span>`;
|
|
@ -1,4 +1,20 @@
|
|||
import { App, Plugin } from 'vue';
|
||||
import Avatar from './Avatar';
|
||||
import { withInstall } from '../_util/type';
|
||||
import Group from './Group';
|
||||
|
||||
export default withInstall(Avatar);
|
||||
export { AvatarProps, AvatarSize, avatarProps } from './Avatar';
|
||||
export { AvatarGroupProps } from './Group';
|
||||
|
||||
Avatar.Group = Group;
|
||||
|
||||
/* istanbul ignore next */
|
||||
Avatar.install = function(app: App) {
|
||||
app.component(Avatar.name, Avatar);
|
||||
app.component(Group.name, Group);
|
||||
return app;
|
||||
};
|
||||
|
||||
export default Avatar as typeof Avatar &
|
||||
Plugin & {
|
||||
readonly Group: typeof Group;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
.@{avatar-prefix-cls}-group {
|
||||
display: inline-flex;
|
||||
|
||||
.@{avatar-prefix-cls} {
|
||||
border: 1px solid @avatar-group-border-color;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: @avatar-group-overlapping;
|
||||
}
|
||||
}
|
||||
|
||||
&-popover {
|
||||
.@{ant-prefix}-avatar + .@{ant-prefix}-avatar {
|
||||
margin-left: @avatar-group-space;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,19 +7,22 @@
|
|||
.reset-component();
|
||||
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
color: @avatar-color;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
background: @avatar-bg;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&-image {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.@{ant-prefix}-image-img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.avatar-size(@avatar-size-base, @avatar-font-size-base);
|
||||
|
||||
&-lg {
|
||||
|
@ -45,6 +48,7 @@
|
|||
.avatar-size(@size, @font-size) {
|
||||
width: @size;
|
||||
height: @size;
|
||||
line-height: @size;
|
||||
border-radius: 50%;
|
||||
|
||||
&-string {
|
||||
|
@ -55,8 +59,12 @@
|
|||
|
||||
&.@{avatar-prefix-cls}-icon {
|
||||
font-size: @font-size;
|
||||
.@{iconfont-css-prefix} {
|
||||
|
||||
> .@{iconfont-css-prefix} {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@import './group';
|
||||
@import './rtl';
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
.@{avatar-prefix-cls}-group {
|
||||
&-rtl {
|
||||
.@{avatar-prefix-cls}:not(:first-child) {
|
||||
margin-right: @avatar-group-overlapping;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-popover.@{ant-prefix}-popover-rtl {
|
||||
.@{ant-prefix}-avatar + .@{ant-prefix}-avatar {
|
||||
margin-right: @avatar-group-space;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import PropTypes from '../_util/vue-types';
|
||||
export default () => ({
|
||||
visibilityHeight: PropTypes.number,
|
||||
// onClick?: React.MouseEventHandler<any>;
|
||||
target: PropTypes.func,
|
||||
prefixCls: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
// visible: PropTypes.looseBool, // Only for test. Don't use it.
|
||||
});
|
|
@ -1,108 +1,147 @@
|
|||
import { defineComponent, inject, nextTick } from 'vue';
|
||||
import classNames from '../_util/classNames';
|
||||
import {
|
||||
defineComponent,
|
||||
ExtractPropTypes,
|
||||
inject,
|
||||
nextTick,
|
||||
onActivated,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
reactive,
|
||||
PropType,
|
||||
ref,
|
||||
watch,
|
||||
onDeactivated,
|
||||
computed,
|
||||
} from 'vue';
|
||||
import VerticalAlignTopOutlined from '@ant-design/icons-vue/VerticalAlignTopOutlined';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import backTopTypes from './backTopTypes';
|
||||
import addEventListener from '../vc-util/Dom/addEventListener';
|
||||
import getScroll from '../_util/getScroll';
|
||||
import BaseMixin from '../_util/BaseMixin';
|
||||
import { getTransitionProps, Transition } from '../_util/transition';
|
||||
import { defaultConfigProvider } from '../config-provider';
|
||||
import scrollTo from '../_util/scrollTo';
|
||||
import { withInstall } from '../_util/type';
|
||||
import throttleByAnimationFrame from '../_util/throttleByAnimationFrame';
|
||||
|
||||
function getDefaultTarget() {
|
||||
return window;
|
||||
}
|
||||
export const backTopProps = {
|
||||
visibilityHeight: PropTypes.number.def(400),
|
||||
duration: PropTypes.number.def(450),
|
||||
target: Function as PropType<() => HTMLElement | Window | Document>,
|
||||
prefixCls: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
// visible: PropTypes.looseBool, // Only for test. Don't use it.
|
||||
};
|
||||
|
||||
const props = backTopTypes();
|
||||
export type BackTopProps = Partial<ExtractPropTypes<typeof backTopProps>>;
|
||||
|
||||
const BackTop = defineComponent({
|
||||
name: 'ABackTop',
|
||||
mixins: [BaseMixin],
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
...props,
|
||||
visibilityHeight: PropTypes.number.def(400),
|
||||
},
|
||||
props: backTopProps,
|
||||
emits: ['click'],
|
||||
setup() {
|
||||
return {
|
||||
configProvider: inject('configProvider', defaultConfigProvider),
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
setup(props, { slots, attrs, emit }) {
|
||||
const configProvider = inject('configProvider', defaultConfigProvider);
|
||||
const domRef = ref();
|
||||
const state = reactive({
|
||||
visible: false,
|
||||
scrollEvent: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
nextTick(() => {
|
||||
const getTarget = this.target || getDefaultTarget;
|
||||
this.scrollEvent = addEventListener(getTarget(), 'scroll', this.handleScroll);
|
||||
this.handleScroll();
|
||||
});
|
||||
},
|
||||
|
||||
activated() {
|
||||
nextTick(() => {
|
||||
this.handleScroll();
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.scrollEvent) {
|
||||
this.scrollEvent.remove();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getCurrentScrollTop() {
|
||||
const getTarget = this.target || getDefaultTarget;
|
||||
const targetNode = getTarget();
|
||||
if (targetNode === window) {
|
||||
return window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop;
|
||||
}
|
||||
return targetNode.scrollTop;
|
||||
},
|
||||
const getDefaultTarget = () =>
|
||||
domRef.value && domRef.value.ownerDocument ? domRef.value.ownerDocument : window;
|
||||
|
||||
scrollToTop(e: Event) {
|
||||
const { target = getDefaultTarget } = this;
|
||||
const scrollToTop = (e: Event) => {
|
||||
const { target = getDefaultTarget, duration } = props;
|
||||
scrollTo(0, {
|
||||
getContainer: target,
|
||||
duration,
|
||||
});
|
||||
this.$emit('click', e);
|
||||
},
|
||||
|
||||
handleScroll() {
|
||||
const { visibilityHeight, target = getDefaultTarget } = this;
|
||||
const scrollTop = getScroll(target(), true);
|
||||
this.setState({
|
||||
visible: scrollTop > visibilityHeight,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
render() {
|
||||
const { prefixCls: customizePrefixCls, $slots } = this;
|
||||
|
||||
const getPrefixCls = this.configProvider.getPrefixCls;
|
||||
const prefixCls = getPrefixCls('back-top', customizePrefixCls);
|
||||
const classString = classNames(prefixCls, this.$attrs.class);
|
||||
const defaultElement = (
|
||||
<div class={`${prefixCls}-content`}>
|
||||
<div class={`${prefixCls}-icon`} />
|
||||
</div>
|
||||
);
|
||||
const divProps = {
|
||||
...this.$attrs,
|
||||
onClick: this.scrollToTop,
|
||||
class: classString,
|
||||
emit('click', e);
|
||||
};
|
||||
|
||||
const backTopBtn = this.visible ? (
|
||||
<div {...divProps}>{$slots.default?.() || defaultElement}</div>
|
||||
) : null;
|
||||
const transitionProps = getTransitionProps('fade');
|
||||
return <Transition {...transitionProps}>{backTopBtn}</Transition>;
|
||||
const handleScroll = throttleByAnimationFrame((e: Event | { target: any }) => {
|
||||
const { visibilityHeight } = props;
|
||||
const scrollTop = getScroll(e.target, true);
|
||||
state.visible = scrollTop > visibilityHeight;
|
||||
});
|
||||
|
||||
const bindScrollEvent = () => {
|
||||
const { target } = props;
|
||||
const getTarget = target || getDefaultTarget;
|
||||
const container = getTarget();
|
||||
state.scrollEvent = addEventListener(container, 'scroll', (e: Event) => {
|
||||
handleScroll(e);
|
||||
});
|
||||
handleScroll({
|
||||
target: container,
|
||||
});
|
||||
};
|
||||
|
||||
const scrollRemove = () => {
|
||||
if (state.scrollEvent) {
|
||||
state.scrollEvent.remove();
|
||||
}
|
||||
(handleScroll as any).cancel();
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.target,
|
||||
() => {
|
||||
scrollRemove();
|
||||
nextTick(() => {
|
||||
bindScrollEvent();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
bindScrollEvent();
|
||||
});
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
nextTick(() => {
|
||||
bindScrollEvent();
|
||||
});
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
scrollRemove();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
scrollRemove();
|
||||
});
|
||||
|
||||
const prefixCls = computed(() => configProvider.getPrefixCls('back-top', props.prefixCls));
|
||||
|
||||
return () => {
|
||||
const defaultElement = (
|
||||
<div class={`${prefixCls.value}-content`}>
|
||||
<div class={`${prefixCls.value}-icon`}>
|
||||
<VerticalAlignTopOutlined />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const divProps = {
|
||||
...attrs,
|
||||
onClick: scrollToTop,
|
||||
class: {
|
||||
[`${prefixCls.value}`]: true,
|
||||
[`${attrs.class}`]: attrs.class,
|
||||
[`${prefixCls.value}-rtl`]: configProvider.direction === 'rtl',
|
||||
},
|
||||
};
|
||||
|
||||
const backTopBtn = state.visible ? (
|
||||
<div {...divProps} ref={domRef}>
|
||||
{slots.default?.() || defaultElement}
|
||||
</div>
|
||||
) : null;
|
||||
const transitionProps = getTransitionProps('fade');
|
||||
return <Transition {...transitionProps}>{backTopBtn}</Transition>;
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -14,6 +14,16 @@
|
|||
height: 40px;
|
||||
cursor: pointer;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-rtl {
|
||||
right: auto;
|
||||
left: 100px;
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
&-content {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
@ -22,20 +32,17 @@
|
|||
text-align: center;
|
||||
background-color: @back-top-bg;
|
||||
border-radius: 20px;
|
||||
transition: all 0.3s @ease-in-out;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: @back-top-hover-bg;
|
||||
transition: all 0.3s @ease-in-out;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
&-icon {
|
||||
width: 14px;
|
||||
height: 16px;
|
||||
margin: 12px auto;
|
||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAoCAYAAACWwljjAAAABGdBTUEAALGPC/xhBQAAAbtJREFUWAntmMtKw0AUhhMvS5cuxILgQlRUpIggIoKIIoigG1eC+AA+jo+i6FIXBfeuXIgoeKVeitVWJX5HWhhDksnUpp3FDPyZk3Nm5nycmZKkXhAEOXSA3lG7muTeRzmfy6HneUvIhnYkQK+Q9NhAA0Opg0vBEhjBKHiyb8iGMyQMOYuK41BcBSypAL+MYXSKjtFAW7EAGEO3qN4uMQbbAkXiSfRQJ1H6a+yhlkKRcAoVFYiweYNjtCVQJJpBz2GCiPt7fBOZQpFgDpUikse5HgnkM4Fi4QX0Fpc5wf9EbLqpUCy4jMoJSXWhFwbMNgWKhVbRhy5jirhs9fy/oFhgHVVTJEs7RLZ8sSEoJm6iz7SZDMbJ+/OKERQTttCXQRLToRUmrKWCYuA2+jbN0MB4OQobYShfdTCgn/sL1K36M7TLrN3n+758aPy2rrpR6+/od5E8tf/A1uLS9aId5T7J3CNYihkQ4D9PiMdMC7mp4rjB9kjFjZp8BlnVHJBuO1yFXIV0FdDF3RlyFdJVQBdv5AxVdIsq8apiZ2PyYO1EVykesGfZEESsCkweyR8MUW+V8uJ1gkYipmpdP1pm2aJVPEGzAAAAAElFTkSuQmCC)
|
||||
~'100%/100%' no-repeat;
|
||||
font-size: 24px;
|
||||
line-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,27 +1,29 @@
|
|||
import PropTypes from '../_util/vue-types';
|
||||
import ScrollNumber from './ScrollNumber';
|
||||
import classNames from '../_util/classNames';
|
||||
import { initDefaultProps, getComponent, getSlot } from '../_util/props-util';
|
||||
import { getPropsSlot, flattenChildren } from '../_util/props-util';
|
||||
import { cloneElement } from '../_util/vnode';
|
||||
import { getTransitionProps, Transition } from '../_util/transition';
|
||||
import isNumeric from '../_util/isNumeric';
|
||||
import { defaultConfigProvider } from '../config-provider';
|
||||
import { inject, defineComponent, CSSProperties, VNode, App, Plugin } from 'vue';
|
||||
import { defineComponent, ExtractPropTypes, CSSProperties, computed, ref, watch } from 'vue';
|
||||
import { tuple } from '../_util/type';
|
||||
import Ribbon from './Ribbon';
|
||||
import { isPresetColor } from './utils';
|
||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||
import isNumeric from '../_util/isNumeric';
|
||||
|
||||
const BadgeProps = {
|
||||
export const badgeProps = {
|
||||
/** Number to show in badge */
|
||||
count: PropTypes.VNodeChild,
|
||||
count: PropTypes.any,
|
||||
showZero: PropTypes.looseBool,
|
||||
/** Max count to show */
|
||||
overflowCount: PropTypes.number,
|
||||
overflowCount: PropTypes.number.def(99),
|
||||
/** whether to show red dot without number */
|
||||
dot: PropTypes.looseBool,
|
||||
prefixCls: PropTypes.string,
|
||||
scrollNumberPrefixCls: PropTypes.string,
|
||||
status: PropTypes.oneOf(tuple('success', 'processing', 'default', 'error', 'warning')),
|
||||
// sync antd@4.6.0
|
||||
size: PropTypes.oneOf(tuple('default', 'small')).def('default'),
|
||||
color: PropTypes.string,
|
||||
text: PropTypes.VNodeChild,
|
||||
offset: PropTypes.arrayOf(PropTypes.oneOfType([String, Number])),
|
||||
|
@ -29,210 +31,192 @@ const BadgeProps = {
|
|||
title: PropTypes.string,
|
||||
};
|
||||
|
||||
const Badge = defineComponent({
|
||||
export type BadgeProps = Partial<ExtractPropTypes<typeof badgeProps>>;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ABadge',
|
||||
Ribbon,
|
||||
props: initDefaultProps(BadgeProps, {
|
||||
showZero: false,
|
||||
dot: false,
|
||||
overflowCount: 99,
|
||||
}) as typeof BadgeProps,
|
||||
setup() {
|
||||
return {
|
||||
configProvider: inject('configProvider', defaultConfigProvider),
|
||||
badgeCount: undefined,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getNumberedDispayCount() {
|
||||
const { overflowCount } = this.$props;
|
||||
const count = this.badgeCount;
|
||||
const displayCount = count > overflowCount ? `${overflowCount}+` : count;
|
||||
return displayCount;
|
||||
},
|
||||
inheritAttrs: false,
|
||||
props: badgeProps,
|
||||
slots: ['text', 'count'],
|
||||
setup(props, { slots, attrs }) {
|
||||
const { prefixCls, direction } = useConfigInject('badge', props);
|
||||
|
||||
getDispayCount() {
|
||||
const isDot = this.isDot();
|
||||
// dot mode don't need count
|
||||
if (isDot) {
|
||||
return '';
|
||||
// ================================ Misc ================================
|
||||
const numberedDisplayCount = computed(() => {
|
||||
return ((props.count as number) > (props.overflowCount as number)
|
||||
? `${props.overflowCount}+`
|
||||
: props.count) as string | number | null;
|
||||
});
|
||||
|
||||
const hasStatus = computed(
|
||||
() =>
|
||||
(props.status !== null && props.status !== undefined) ||
|
||||
(props.color !== null && props.color !== undefined),
|
||||
);
|
||||
|
||||
const isZero = computed(
|
||||
() => numberedDisplayCount.value === '0' || numberedDisplayCount.value === 0,
|
||||
);
|
||||
|
||||
const showAsDot = computed(() => (props.dot && !isZero.value) || hasStatus.value);
|
||||
|
||||
const mergedCount = computed(() => (showAsDot.value ? '' : numberedDisplayCount.value));
|
||||
|
||||
const isHidden = computed(() => {
|
||||
const isEmpty =
|
||||
mergedCount.value === null || mergedCount.value === undefined || mergedCount.value === '';
|
||||
return (isEmpty || (isZero.value && !props.showZero)) && !showAsDot.value;
|
||||
});
|
||||
|
||||
// Count should be cache in case hidden change it
|
||||
const livingCount = ref(props.count);
|
||||
|
||||
// We need cache count since remove motion should not change count display
|
||||
const displayCount = ref(mergedCount.value);
|
||||
|
||||
// We will cache the dot status to avoid shaking on leaved motion
|
||||
const isDotRef = ref(showAsDot.value);
|
||||
|
||||
watch(
|
||||
[() => props.count, mergedCount, showAsDot],
|
||||
() => {
|
||||
if (!isHidden.value) {
|
||||
livingCount.value = props.count;
|
||||
displayCount.value = mergedCount.value;
|
||||
isDotRef.value = showAsDot.value;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// Shared styles
|
||||
const statusCls = computed(() => ({
|
||||
[`${prefixCls.value}-status-dot`]: hasStatus.value,
|
||||
[`${prefixCls.value}-status-${props.status}`]: !!props.status,
|
||||
[`${prefixCls.value}-status-${props.color}`]: isPresetColor(props.color),
|
||||
}));
|
||||
|
||||
const statusStyle = computed(() => {
|
||||
if (props.color && !isPresetColor(props.color)) {
|
||||
return { background: props.color };
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
return this.getNumberedDispayCount();
|
||||
},
|
||||
});
|
||||
|
||||
getScrollNumberTitle() {
|
||||
const { title } = this.$props;
|
||||
const count = this.badgeCount;
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
return typeof count === 'string' || typeof count === 'number' ? count : undefined;
|
||||
},
|
||||
const scrollNumberCls = computed(() => ({
|
||||
[`${prefixCls.value}-dot`]: isDotRef.value,
|
||||
[`${prefixCls.value}-count`]: !isDotRef.value,
|
||||
[`${prefixCls.value}-count-sm`]: props.size === 'small',
|
||||
[`${prefixCls.value}-multiple-words`]:
|
||||
!isDotRef.value && displayCount.value && displayCount.value.toString().length > 1,
|
||||
[`${prefixCls.value}-status-${status}`]: !!status,
|
||||
[`${prefixCls.value}-status-${props.color}`]: isPresetColor(props.color),
|
||||
}));
|
||||
|
||||
getStyleWithOffset() {
|
||||
const { offset, numberStyle } = this.$props;
|
||||
return offset
|
||||
? {
|
||||
right: `${-parseInt(offset[0] as string, 10)}px`,
|
||||
marginTop: isNumeric(offset[1]) ? `${offset[1]}px` : offset[1],
|
||||
...numberStyle,
|
||||
}
|
||||
: { ...numberStyle };
|
||||
},
|
||||
getBadgeClassName(prefixCls: string, children: VNode[]) {
|
||||
const hasStatus = this.hasStatus();
|
||||
return classNames(prefixCls, {
|
||||
[`${prefixCls}-status`]: hasStatus,
|
||||
[`${prefixCls}-dot-status`]: hasStatus && this.dot && !this.isZero(),
|
||||
[`${prefixCls}-not-a-wrapper`]: !children.length,
|
||||
});
|
||||
},
|
||||
hasStatus() {
|
||||
const { status, color } = this.$props;
|
||||
return !!status || !!color;
|
||||
},
|
||||
isZero() {
|
||||
const numberedDispayCount = this.getNumberedDispayCount();
|
||||
return numberedDispayCount === '0' || numberedDispayCount === 0;
|
||||
},
|
||||
return () => {
|
||||
const { offset, title, color } = props;
|
||||
const style = attrs.style as CSSProperties;
|
||||
const text = getPropsSlot(slots, props, 'text');
|
||||
const pre = prefixCls.value;
|
||||
const count = livingCount.value;
|
||||
let children = flattenChildren(slots.default?.());
|
||||
children = children.length ? children : null;
|
||||
|
||||
isDot() {
|
||||
const { dot } = this.$props;
|
||||
const isZero = this.isZero();
|
||||
return (dot && !isZero) || this.hasStatus();
|
||||
},
|
||||
const visible = !!(!isHidden.value || slots.count);
|
||||
|
||||
isHidden() {
|
||||
const { showZero } = this.$props;
|
||||
const displayCount = this.getDispayCount();
|
||||
const isZero = this.isZero();
|
||||
const isDot = this.isDot();
|
||||
const isEmpty = displayCount === null || displayCount === undefined || displayCount === '';
|
||||
return (isEmpty || (isZero && !showZero)) && !isDot;
|
||||
},
|
||||
// =============================== Styles ===============================
|
||||
const mergedStyle = (() => {
|
||||
if (!offset) {
|
||||
return { ...style };
|
||||
}
|
||||
|
||||
renderStatusText(prefixCls: string) {
|
||||
const text = getComponent(this, 'text');
|
||||
const hidden = this.isHidden();
|
||||
return hidden || !text ? null : <span class={`${prefixCls}-status-text`}>{text}</span>;
|
||||
},
|
||||
const offsetStyle: CSSProperties = {
|
||||
marginTop: isNumeric(offset[1]) ? `${offset[1]}px` : offset[1],
|
||||
};
|
||||
if (direction.value === 'rtl') {
|
||||
offsetStyle.left = `${parseInt(offset[0] as string, 10)}px`;
|
||||
} else {
|
||||
offsetStyle.right = `${-parseInt(offset[0] as string, 10)}px`;
|
||||
}
|
||||
|
||||
renderDispayComponent() {
|
||||
const count = this.badgeCount;
|
||||
const customNode = count;
|
||||
if (!customNode || typeof customNode !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
return cloneElement(
|
||||
customNode,
|
||||
return {
|
||||
...offsetStyle,
|
||||
...style,
|
||||
};
|
||||
})();
|
||||
|
||||
// =============================== Render ===============================
|
||||
// >>> Title
|
||||
const titleNode =
|
||||
title ?? (typeof count === 'string' || typeof count === 'number' ? count : undefined);
|
||||
|
||||
// >>> Status Text
|
||||
const statusTextNode =
|
||||
visible || !text ? null : <span class={`${pre}-status-text`}>{text}</span>;
|
||||
|
||||
// >>> Display Component
|
||||
const displayNode = cloneElement(
|
||||
slots.count?.(),
|
||||
{
|
||||
style: this.getStyleWithOffset(),
|
||||
style: mergedStyle,
|
||||
},
|
||||
false,
|
||||
);
|
||||
},
|
||||
|
||||
renderBadgeNumber(prefixCls: string, scrollNumberPrefixCls: string) {
|
||||
const { status, color } = this.$props;
|
||||
const count = this.badgeCount;
|
||||
const displayCount = this.getDispayCount();
|
||||
const isDot = this.isDot();
|
||||
const hidden = this.isHidden();
|
||||
const badgeClassName = classNames(
|
||||
pre,
|
||||
{
|
||||
[`${pre}-status`]: hasStatus.value,
|
||||
[`${pre}-not-a-wrapper`]: !children,
|
||||
[`${pre}-rtl`]: direction.value === 'rtl',
|
||||
},
|
||||
attrs.class,
|
||||
);
|
||||
|
||||
const scrollNumberCls = {
|
||||
[`${prefixCls}-dot`]: isDot,
|
||||
[`${prefixCls}-count`]: !isDot,
|
||||
[`${prefixCls}-multiple-words`]:
|
||||
!isDot && count && count.toString && count.toString().length > 1,
|
||||
[`${prefixCls}-status-${status}`]: !!status,
|
||||
[`${prefixCls}-status-${color}`]: isPresetColor(color),
|
||||
};
|
||||
|
||||
let statusStyle = this.getStyleWithOffset();
|
||||
if (color && !isPresetColor(color)) {
|
||||
statusStyle = statusStyle || {};
|
||||
statusStyle.background = color;
|
||||
// <Badge status="success" />
|
||||
if (!children && hasStatus.value) {
|
||||
const statusTextColor = mergedStyle.color;
|
||||
return (
|
||||
<span {...attrs} class={badgeClassName} style={mergedStyle}>
|
||||
<span class={statusCls.value} style={statusStyle.value} />
|
||||
<span style={{ color: statusTextColor }} class={`${pre}-status-text`}>
|
||||
{text}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return hidden ? null : (
|
||||
<ScrollNumber
|
||||
prefixCls={scrollNumberPrefixCls}
|
||||
data-show={!hidden}
|
||||
v-show={!hidden}
|
||||
class={scrollNumberCls}
|
||||
count={displayCount}
|
||||
displayComponent={this.renderDispayComponent()}
|
||||
title={this.getScrollNumberTitle()}
|
||||
style={statusStyle}
|
||||
key="scrollNumber"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
const transitionProps = getTransitionProps(children ? `${pre}-zoom` : '', {
|
||||
appear: false,
|
||||
});
|
||||
let scrollNumberStyle: CSSProperties = { ...mergedStyle, ...props.numberStyle };
|
||||
if (color && !isPresetColor(color)) {
|
||||
scrollNumberStyle = scrollNumberStyle || {};
|
||||
scrollNumberStyle.background = color;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
scrollNumberPrefixCls: customizeScrollNumberPrefixCls,
|
||||
status,
|
||||
color,
|
||||
} = this;
|
||||
|
||||
const text = getComponent(this, 'text');
|
||||
const getPrefixCls = this.configProvider.getPrefixCls;
|
||||
const prefixCls = getPrefixCls('badge', customizePrefixCls);
|
||||
const scrollNumberPrefixCls = getPrefixCls('scroll-number', customizeScrollNumberPrefixCls);
|
||||
|
||||
const children = getSlot(this);
|
||||
let count = getComponent(this, 'count');
|
||||
if (Array.isArray(count)) {
|
||||
count = count[0];
|
||||
}
|
||||
this.badgeCount = count;
|
||||
const scrollNumber = this.renderBadgeNumber(prefixCls, scrollNumberPrefixCls);
|
||||
const statusText = this.renderStatusText(prefixCls);
|
||||
const statusCls = classNames({
|
||||
[`${prefixCls}-status-dot`]: this.hasStatus(),
|
||||
[`${prefixCls}-status-${status}`]: !!status,
|
||||
[`${prefixCls}-status-${color}`]: isPresetColor(color),
|
||||
});
|
||||
const statusStyle: CSSProperties = {};
|
||||
if (color && !isPresetColor(color)) {
|
||||
statusStyle.background = color;
|
||||
}
|
||||
// <Badge status="success" />
|
||||
if (!children.length && this.hasStatus()) {
|
||||
const styleWithOffset = this.getStyleWithOffset();
|
||||
const statusTextColor = styleWithOffset && styleWithOffset.color;
|
||||
return (
|
||||
<span class={this.getBadgeClassName(prefixCls, children)} style={styleWithOffset}>
|
||||
<span class={statusCls} style={statusStyle} />
|
||||
<span style={{ color: statusTextColor }} class={`${prefixCls}-status-text`}>
|
||||
{text}
|
||||
</span>
|
||||
<span {...attrs} class={badgeClassName}>
|
||||
{children}
|
||||
<Transition {...transitionProps}>
|
||||
<ScrollNumber
|
||||
v-show={visible}
|
||||
prefixCls={props.scrollNumberPrefixCls}
|
||||
show={visible}
|
||||
class={scrollNumberCls.value}
|
||||
count={displayCount.value}
|
||||
title={titleNode}
|
||||
style={scrollNumberStyle}
|
||||
key="scrollNumber"
|
||||
>
|
||||
{displayNode}
|
||||
</ScrollNumber>
|
||||
</Transition>
|
||||
{statusTextNode}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const transitionProps = getTransitionProps(children.length ? `${prefixCls}-zoom` : '');
|
||||
|
||||
return (
|
||||
<span class={this.getBadgeClassName(prefixCls, children)}>
|
||||
{children}
|
||||
<Transition {...transitionProps}>{scrollNumber}</Transition>
|
||||
{statusText}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Badge.install = function(app: App) {
|
||||
app.component(Badge.name, Badge);
|
||||
app.component(Badge.Ribbon.displayName, Badge.Ribbon);
|
||||
return app;
|
||||
};
|
||||
|
||||
export default Badge as typeof Badge &
|
||||
Plugin & {
|
||||
readonly Ribbon: typeof Ribbon;
|
||||
};
|
||||
|
|
|
@ -1,60 +1,55 @@
|
|||
import { LiteralUnion, tuple } from '../_util/type';
|
||||
import { PresetColorType } from '../_util/colors';
|
||||
import { isPresetColor } from './utils';
|
||||
import { defaultConfigProvider } from '../config-provider';
|
||||
import { HTMLAttributes, FunctionalComponent, VNodeTypes, inject, CSSProperties } from 'vue';
|
||||
import { CSSProperties, defineComponent, PropType, ExtractPropTypes, computed } from 'vue';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||
|
||||
type RibbonPlacement = 'start' | 'end';
|
||||
|
||||
export interface RibbonProps extends HTMLAttributes {
|
||||
prefixCls?: string;
|
||||
text?: VNodeTypes;
|
||||
color?: LiteralUnion<PresetColorType, string>;
|
||||
placement?: RibbonPlacement;
|
||||
}
|
||||
|
||||
const Ribbon: FunctionalComponent<RibbonProps> = (props, { attrs, slots }) => {
|
||||
const { prefixCls: customizePrefixCls, color, text = slots.text?.(), placement = 'end' } = props;
|
||||
const { class: className, style } = attrs;
|
||||
const children = slots.default?.();
|
||||
const { getPrefixCls, direction } = inject('configProvider', defaultConfigProvider);
|
||||
|
||||
const prefixCls = getPrefixCls('ribbon', customizePrefixCls);
|
||||
const colorInPreset = isPresetColor(color);
|
||||
const ribbonCls = [
|
||||
prefixCls,
|
||||
`${prefixCls}-placement-${placement}`,
|
||||
{
|
||||
[`${prefixCls}-rtl`]: direction === 'rtl',
|
||||
[`${prefixCls}-color-${color}`]: colorInPreset,
|
||||
},
|
||||
className,
|
||||
];
|
||||
const colorStyle: CSSProperties = {};
|
||||
const cornerColorStyle: CSSProperties = {};
|
||||
if (color && !colorInPreset) {
|
||||
colorStyle.background = color;
|
||||
cornerColorStyle.color = color;
|
||||
}
|
||||
return (
|
||||
<div class={`${prefixCls}-wrapper`}>
|
||||
{children}
|
||||
<div class={ribbonCls} style={{ ...colorStyle, ...(style as CSSProperties) }}>
|
||||
<span class={`${prefixCls}-text`}>{text}</span>
|
||||
<div class={`${prefixCls}-corner`} style={cornerColorStyle} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Ribbon.displayName = 'ABadgeRibbon';
|
||||
Ribbon.inheritAttrs = false;
|
||||
Ribbon.props = {
|
||||
const ribbonProps = {
|
||||
prefix: PropTypes.string,
|
||||
color: PropTypes.string,
|
||||
color: { type: String as PropType<LiteralUnion<PresetColorType, string>> },
|
||||
text: PropTypes.any,
|
||||
placement: PropTypes.oneOf(tuple('start', 'end')),
|
||||
placement: PropTypes.oneOf(tuple('start', 'end')).def('end'),
|
||||
};
|
||||
|
||||
export default Ribbon;
|
||||
export type RibbonProps = Partial<ExtractPropTypes<typeof ribbonProps>>;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ABadgeRibbon',
|
||||
inheritAttrs: false,
|
||||
props: ribbonProps,
|
||||
slots: ['text'],
|
||||
setup(props, { attrs, slots }) {
|
||||
const { prefixCls, direction } = useConfigInject('ribbon', props);
|
||||
const colorInPreset = computed(() => isPresetColor(props.color));
|
||||
const ribbonCls = computed(() => [
|
||||
prefixCls.value,
|
||||
`${prefixCls.value}-placement-${props.placement}`,
|
||||
{
|
||||
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
|
||||
[`${prefixCls.value}-color-${props.color}`]: colorInPreset.value,
|
||||
},
|
||||
]);
|
||||
return () => {
|
||||
const { class: className, style, ...restAttrs } = attrs;
|
||||
const colorStyle: CSSProperties = {};
|
||||
const cornerColorStyle: CSSProperties = {};
|
||||
if (props.color && !colorInPreset.value) {
|
||||
colorStyle.background = props.color;
|
||||
cornerColorStyle.color = props.color;
|
||||
}
|
||||
return (
|
||||
<div class={`${prefixCls.value}-wrapper`} {...restAttrs}>
|
||||
{slots.default?.()}
|
||||
<div
|
||||
class={[ribbonCls.value, className]}
|
||||
style={{ ...colorStyle, ...(style as CSSProperties) }}
|
||||
>
|
||||
<span class={`${prefixCls.value}-text`}>{props.text || slots.text?.()}</span>
|
||||
<div class={`${prefixCls.value}-corner`} style={cornerColorStyle} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,203 +1,90 @@
|
|||
import classNames from '../_util/classNames';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import BaseMixin from '../_util/BaseMixin';
|
||||
import omit from 'omit.js';
|
||||
import { cloneElement } from '../_util/vnode';
|
||||
import { defaultConfigProvider } from '../config-provider';
|
||||
import { CSSProperties, defineComponent, inject } from 'vue';
|
||||
import {
|
||||
defineComponent,
|
||||
ExtractPropTypes,
|
||||
CSSProperties,
|
||||
DefineComponent,
|
||||
HTMLAttributes,
|
||||
} from 'vue';
|
||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||
import SingleNumber from './SingleNumber';
|
||||
import { filterEmpty } from '../_util/props-util';
|
||||
|
||||
function getNumberArray(num: string | number | undefined | null) {
|
||||
return num
|
||||
? num
|
||||
.toString()
|
||||
.split('')
|
||||
.reverse()
|
||||
.map(i => {
|
||||
const current = Number(i);
|
||||
return isNaN(current) ? i : current;
|
||||
})
|
||||
: [];
|
||||
}
|
||||
|
||||
const ScrollNumberProps = {
|
||||
export const scrollNumberProps = {
|
||||
prefixCls: PropTypes.string,
|
||||
count: PropTypes.any,
|
||||
component: PropTypes.string,
|
||||
title: PropTypes.oneOfType([PropTypes.number, PropTypes.string, null]),
|
||||
displayComponent: PropTypes.any,
|
||||
onAnimated: PropTypes.func,
|
||||
show: Boolean,
|
||||
};
|
||||
|
||||
export type ScrollNumberProps = Partial<ExtractPropTypes<typeof scrollNumberProps>>;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ScrollNumber',
|
||||
mixins: [BaseMixin],
|
||||
inheritAttrs: false,
|
||||
props: ScrollNumberProps,
|
||||
emits: ['animated'],
|
||||
setup() {
|
||||
return {
|
||||
configProvider: inject('configProvider', defaultConfigProvider),
|
||||
lastCount: undefined,
|
||||
timeout: undefined,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
animateStarted: true,
|
||||
sCount: this.count,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
count() {
|
||||
this.lastCount = this.sCount;
|
||||
this.setState({
|
||||
animateStarted: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
updated() {
|
||||
const { animateStarted, count } = this;
|
||||
if (animateStarted) {
|
||||
this.clearTimeout();
|
||||
// Let browser has time to reset the scroller before actually
|
||||
// performing the transition.
|
||||
this.timeout = setTimeout(() => {
|
||||
this.setState(
|
||||
{
|
||||
animateStarted: false,
|
||||
sCount: count,
|
||||
},
|
||||
this.handleAnimated,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.clearTimeout();
|
||||
},
|
||||
methods: {
|
||||
clearTimeout() {
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = undefined;
|
||||
}
|
||||
},
|
||||
getPositionByNum(num: number, i: number) {
|
||||
const { sCount } = this;
|
||||
const currentCount = Math.abs(Number(sCount));
|
||||
const lastCount = Math.abs(Number(this.lastCount));
|
||||
const currentDigit = Math.abs(getNumberArray(sCount)[i] as number);
|
||||
const lastDigit = Math.abs(getNumberArray(this.lastCount)[i] as number);
|
||||
props: scrollNumberProps,
|
||||
setup(props, { attrs, slots }) {
|
||||
const { prefixCls } = useConfigInject('scroll-number', props);
|
||||
|
||||
if (this.animateStarted) {
|
||||
return 10 + num;
|
||||
}
|
||||
// 同方向则在同一侧切换数字
|
||||
if (currentCount > lastCount) {
|
||||
if (currentDigit >= lastDigit) {
|
||||
return 10 + num;
|
||||
}
|
||||
return 20 + num;
|
||||
}
|
||||
if (currentDigit <= lastDigit) {
|
||||
return 10 + num;
|
||||
}
|
||||
return num;
|
||||
},
|
||||
handleAnimated() {
|
||||
this.$emit('animated');
|
||||
},
|
||||
return () => {
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
count,
|
||||
title,
|
||||
show,
|
||||
component: Tag = ('sup' as unknown) as DefineComponent,
|
||||
class: className,
|
||||
style,
|
||||
...restProps
|
||||
} = { ...props, ...attrs } as ScrollNumberProps & HTMLAttributes & { style: CSSProperties };
|
||||
// ============================ Render ============================
|
||||
const newProps = {
|
||||
...restProps,
|
||||
style,
|
||||
'data-show': props.show,
|
||||
class: classNames(prefixCls.value, className),
|
||||
title: title as string,
|
||||
};
|
||||
|
||||
renderNumberList(position: number, className: string) {
|
||||
const childrenToReturn = [];
|
||||
for (let i = 0; i < 30; i++) {
|
||||
childrenToReturn.push(
|
||||
<p
|
||||
key={i.toString()}
|
||||
class={classNames(className, {
|
||||
current: position === i,
|
||||
})}
|
||||
>
|
||||
{i % 10}
|
||||
</p>,
|
||||
);
|
||||
// Only integer need motion
|
||||
let numberNodes: any = count;
|
||||
if (count && Number(count) % 1 === 0) {
|
||||
const numberList = String(count).split('');
|
||||
|
||||
numberNodes = numberList.map((num, i) => (
|
||||
<SingleNumber
|
||||
prefixCls={prefixCls.value}
|
||||
count={Number(count)}
|
||||
value={num}
|
||||
key={numberList.length - i}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
return childrenToReturn;
|
||||
},
|
||||
renderCurrentNumber(prefixCls: string, num: number | string, i: number) {
|
||||
if (typeof num === 'number') {
|
||||
const position = this.getPositionByNum(num, i);
|
||||
const removeTransition =
|
||||
this.animateStarted || getNumberArray(this.lastCount)[i] === undefined;
|
||||
const style = {
|
||||
transition: removeTransition ? 'none' : undefined,
|
||||
msTransform: `translateY(${-position * 100}%)`,
|
||||
WebkitTransform: `translateY(${-position * 100}%)`,
|
||||
transform: `translateY(${-position * 100}%)`,
|
||||
// allow specify the border
|
||||
// mock border-color by box-shadow for compatible with old usage:
|
||||
// <Badge count={4} style={{ backgroundColor: '#fff', color: '#999', borderColor: '#d9d9d9' }} />
|
||||
if (style && style.borderColor) {
|
||||
newProps.style = {
|
||||
...(style as CSSProperties),
|
||||
boxShadow: `0 0 0 1px ${style.borderColor} inset`,
|
||||
};
|
||||
return (
|
||||
<span class={`${prefixCls}-only`} style={style} key={i}>
|
||||
{this.renderNumberList(position, `${prefixCls}-only-unit`)}
|
||||
</span>
|
||||
}
|
||||
const children = filterEmpty(slots.default?.());
|
||||
if (children && children.length) {
|
||||
return cloneElement(
|
||||
children,
|
||||
{
|
||||
class: classNames(`${prefixCls.value}-custom-component`),
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span key="symbol" class={`${prefixCls}-symbol`}>
|
||||
{num}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
||||
renderNumberElement(prefixCls: string) {
|
||||
const { sCount } = this;
|
||||
if (sCount && Number(sCount) % 1 === 0) {
|
||||
return getNumberArray(sCount)
|
||||
.map((num, i) => this.renderCurrentNumber(prefixCls, num, i))
|
||||
.reverse();
|
||||
}
|
||||
return sCount;
|
||||
},
|
||||
},
|
||||
|
||||
render() {
|
||||
const { prefixCls: customizePrefixCls, title, component: Tag = 'sup', displayComponent } = this;
|
||||
const getPrefixCls = this.configProvider.getPrefixCls;
|
||||
const prefixCls = getPrefixCls('scroll-number', customizePrefixCls);
|
||||
const { class: className, style = {} } = this.$attrs as {
|
||||
class?: string;
|
||||
style?: CSSProperties;
|
||||
return <Tag {...newProps}>{numberNodes}</Tag>;
|
||||
};
|
||||
if (displayComponent) {
|
||||
return cloneElement(displayComponent, {
|
||||
class: classNames(
|
||||
`${prefixCls}-custom-component`,
|
||||
displayComponent.props && displayComponent.props.class,
|
||||
),
|
||||
});
|
||||
}
|
||||
// fix https://fb.me/react-unknown-prop
|
||||
const restProps = omit({ ...this.$props, ...this.$attrs }, [
|
||||
'count',
|
||||
'onAnimated',
|
||||
'component',
|
||||
'prefixCls',
|
||||
'displayComponent',
|
||||
]);
|
||||
const tempStyle = { ...style };
|
||||
const newProps = {
|
||||
...restProps,
|
||||
title,
|
||||
style: tempStyle,
|
||||
class: classNames(prefixCls, className),
|
||||
};
|
||||
// allow specify the border
|
||||
// mock border-color by box-shadow for compatible with old usage:
|
||||
// <Badge count={4} style={{ backgroundColor: '#fff', color: '#999', borderColor: '#d9d9d9' }} />
|
||||
if (style && style.borderColor) {
|
||||
newProps.style.boxShadow = `0 0 0 1px ${style.borderColor} inset`;
|
||||
}
|
||||
|
||||
return <Tag {...newProps}>{this.renderNumberElement(prefixCls)}</Tag>;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
import { computed, CSSProperties, defineComponent, onUnmounted, reactive, ref, watch } from 'vue';
|
||||
import classNames from '../_util/classNames';
|
||||
|
||||
export interface UnitNumberProps {
|
||||
prefixCls: string;
|
||||
value: string | number;
|
||||
offset?: number;
|
||||
current?: boolean;
|
||||
}
|
||||
|
||||
function UnitNumber({ prefixCls, value, current, offset = 0 }: UnitNumberProps) {
|
||||
let style: CSSProperties | undefined;
|
||||
|
||||
if (offset) {
|
||||
style = {
|
||||
position: 'absolute',
|
||||
top: `${offset}00%`,
|
||||
left: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
style={style}
|
||||
class={classNames(`${prefixCls}-only-unit`, {
|
||||
current,
|
||||
})}
|
||||
>
|
||||
{value}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function getOffset(start: number, end: number, unit: -1 | 1) {
|
||||
let index = start;
|
||||
let offset = 0;
|
||||
|
||||
while ((index + 10) % 10 !== end) {
|
||||
index += unit;
|
||||
offset += unit;
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SingleNumber',
|
||||
props: {
|
||||
prefixCls: String,
|
||||
value: String,
|
||||
count: Number,
|
||||
},
|
||||
setup(props) {
|
||||
const originValue = computed(() => Number(props.value));
|
||||
const originCount = computed(() => Math.abs(props.count));
|
||||
const state = reactive({
|
||||
prevValue: originValue.value,
|
||||
prevCount: originCount.value,
|
||||
});
|
||||
|
||||
// ============================= Events =============================
|
||||
const onTransitionEnd = () => {
|
||||
state.prevValue = originValue.value;
|
||||
state.prevCount = originCount.value;
|
||||
};
|
||||
const timeout = ref();
|
||||
// Fallback if transition event not support
|
||||
watch(
|
||||
originValue,
|
||||
() => {
|
||||
clearTimeout(timeout.value);
|
||||
timeout.value = setTimeout(() => {
|
||||
onTransitionEnd();
|
||||
}, 1000);
|
||||
},
|
||||
{ flush: 'post' },
|
||||
);
|
||||
onUnmounted(() => {
|
||||
clearTimeout(timeout.value);
|
||||
});
|
||||
|
||||
return () => {
|
||||
let unitNodes: any[];
|
||||
let offsetStyle: CSSProperties = {};
|
||||
const value = originValue.value;
|
||||
if (state.prevValue === value || Number.isNaN(value) || Number.isNaN(state.prevValue)) {
|
||||
// Nothing to change
|
||||
unitNodes = [UnitNumber({ ...props, current: true } as UnitNumberProps)];
|
||||
offsetStyle = {
|
||||
transition: 'none',
|
||||
};
|
||||
} else {
|
||||
unitNodes = [];
|
||||
|
||||
// Fill basic number units
|
||||
const end = value + 10;
|
||||
const unitNumberList: number[] = [];
|
||||
for (let index = value; index <= end; index += 1) {
|
||||
unitNumberList.push(index);
|
||||
}
|
||||
|
||||
// Fill with number unit nodes
|
||||
const prevIndex = unitNumberList.findIndex(n => n % 10 === state.prevValue);
|
||||
unitNodes = unitNumberList.map((n, index) => {
|
||||
const singleUnit = n % 10;
|
||||
return UnitNumber({
|
||||
...props,
|
||||
value: singleUnit,
|
||||
offset: index - prevIndex,
|
||||
current: index === prevIndex,
|
||||
} as UnitNumberProps);
|
||||
});
|
||||
|
||||
// Calculate container offset value
|
||||
const unit = state.prevCount < originCount.value ? 1 : -1;
|
||||
offsetStyle = {
|
||||
transform: `translateY(${-getOffset(state.prevValue, value, unit)}00%)`,
|
||||
};
|
||||
}
|
||||
return (
|
||||
<span
|
||||
class={`${props.prefixCls}-only`}
|
||||
style={offsetStyle}
|
||||
onTransitionend={() => onTransitionEnd()}
|
||||
>
|
||||
{unitNodes}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -1,3 +1,14 @@
|
|||
import { App, Plugin } from 'vue';
|
||||
import Badge from './Badge';
|
||||
import Ribbon from './Ribbon';
|
||||
|
||||
export default Badge;
|
||||
Badge.install = function(app: App) {
|
||||
app.component(Badge.name, Badge);
|
||||
app.component(Ribbon.name, Ribbon);
|
||||
return app;
|
||||
};
|
||||
|
||||
export default Badge as typeof Badge &
|
||||
Plugin & {
|
||||
readonly Ribbon: typeof Ribbon;
|
||||
};
|
||||
|
|
|
@ -9,10 +9,10 @@
|
|||
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
color: unset;
|
||||
line-height: 1;
|
||||
|
||||
&-count {
|
||||
z-index: @zindex-badge;
|
||||
min-width: @badge-height;
|
||||
height: @badge-height;
|
||||
padding: 0 6px;
|
||||
|
@ -22,7 +22,7 @@
|
|||
line-height: @badge-height;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
background: @highlight-color;
|
||||
background: @badge-color;
|
||||
border-radius: (@badge-height / 2);
|
||||
box-shadow: 0 0 0 1px @shadow-color-inverse;
|
||||
a,
|
||||
|
@ -31,12 +31,23 @@
|
|||
}
|
||||
}
|
||||
|
||||
&-count-sm {
|
||||
min-width: @badge-height-sm;
|
||||
height: @badge-height-sm;
|
||||
padding: 0;
|
||||
font-size: @badge-font-size-sm;
|
||||
line-height: @badge-height-sm;
|
||||
border-radius: (@badge-height-sm / 2);
|
||||
}
|
||||
|
||||
&-multiple-words {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
&-dot {
|
||||
z-index: @zindex-badge;
|
||||
width: @badge-dot-size;
|
||||
min-width: @badge-dot-size;
|
||||
height: @badge-dot-size;
|
||||
background: @highlight-color;
|
||||
border-radius: 100%;
|
||||
|
@ -49,9 +60,12 @@
|
|||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: @zindex-badge;
|
||||
transform: translate(50%, -50%);
|
||||
transform-origin: 100% 0%;
|
||||
|
||||
&.@{iconfont-css-prefix}-spin {
|
||||
animation: antBadgeLoadingCircle 1s infinite linear;
|
||||
}
|
||||
}
|
||||
|
||||
&-status {
|
||||
|
@ -115,24 +129,39 @@
|
|||
|
||||
&-zoom-appear,
|
||||
&-zoom-enter {
|
||||
animation: antZoomBadgeIn 0.3s @ease-out-back;
|
||||
animation: antZoomBadgeIn @animation-duration-slow @ease-out-back;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
&-zoom-leave {
|
||||
animation: antZoomBadgeOut 0.3s @ease-in-back;
|
||||
animation: antZoomBadgeOut @animation-duration-slow @ease-in-back;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
&-not-a-wrapper {
|
||||
.@{badge-prefix-cls}-zoom-appear,
|
||||
.@{badge-prefix-cls}-zoom-enter {
|
||||
animation: antNoWrapperZoomBadgeIn @animation-duration-slow @ease-out-back;
|
||||
}
|
||||
|
||||
.@{badge-prefix-cls}-zoom-leave {
|
||||
animation: antNoWrapperZoomBadgeOut @animation-duration-slow @ease-in-back;
|
||||
}
|
||||
|
||||
&:not(.@{badge-prefix-cls}-status) {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.@{number-prefix-cls}-custom-component {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.@{number-prefix-cls}-custom-component,
|
||||
.@{ant-prefix}-scroll-number {
|
||||
position: relative;
|
||||
top: auto;
|
||||
display: block;
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
|
||||
.@{badge-prefix-cls}-count {
|
||||
|
@ -152,15 +181,25 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Safari will blink with transform when inner element has absolute style.
|
||||
.safari-fix-motion() {
|
||||
-webkit-transform-style: preserve-3d;
|
||||
-webkit-backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.@{number-prefix-cls} {
|
||||
overflow: hidden;
|
||||
&-only {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: @badge-height;
|
||||
transition: all 0.3s @ease-in-out;
|
||||
transition: all @animation-duration-slow @ease-in-out;
|
||||
.safari-fix-motion;
|
||||
|
||||
> p.@{number-prefix-cls}-only-unit {
|
||||
height: @badge-height;
|
||||
margin: 0;
|
||||
.safari-fix-motion;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -189,4 +228,36 @@
|
|||
}
|
||||
}
|
||||
|
||||
@keyframes antNoWrapperZoomBadgeIn {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes antNoWrapperZoomBadgeOut {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes antBadgeLoadingCircle {
|
||||
0% {
|
||||
transform-origin: 50%;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(50%, -50%) rotate(360deg);
|
||||
transform-origin: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@import './ribbon';
|
||||
@import './rtl';
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
.@{badge-prefix-cls} {
|
||||
&-rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
&-count,
|
||||
&-dot,
|
||||
.@{number-prefix-cls}-custom-component {
|
||||
.@{badge-prefix-cls}-rtl & {
|
||||
right: auto;
|
||||
left: 0;
|
||||
direction: ltr;
|
||||
transform: translate(-50%, -50%);
|
||||
transform-origin: 0% 0%;
|
||||
}
|
||||
}
|
||||
|
||||
.@{badge-prefix-cls}-rtl& .@{number-prefix-cls}-custom-component {
|
||||
right: auto;
|
||||
left: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
transform-origin: 0% 0%;
|
||||
}
|
||||
|
||||
&-status {
|
||||
&-text {
|
||||
.@{badge-prefix-cls}-rtl & {
|
||||
margin-right: 8px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-zoom-appear,
|
||||
&-zoom-enter {
|
||||
.@{badge-prefix-cls}-rtl & {
|
||||
animation-name: antZoomBadgeInRtl;
|
||||
}
|
||||
}
|
||||
|
||||
&-zoom-leave {
|
||||
.@{badge-prefix-cls}-rtl & {
|
||||
animation-name: antZoomBadgeOutRtl;
|
||||
}
|
||||
}
|
||||
|
||||
&-not-a-wrapper {
|
||||
.@{badge-prefix-cls}-count {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{ribbon-prefix-cls}-rtl {
|
||||
direction: rtl;
|
||||
&.@{ribbon-prefix-cls}-placement-end {
|
||||
right: unset;
|
||||
left: -8px;
|
||||
border-bottom-right-radius: @border-radius-sm;
|
||||
border-bottom-left-radius: 0;
|
||||
.@{ribbon-prefix-cls}-corner {
|
||||
right: unset;
|
||||
left: 0;
|
||||
border-color: currentColor currentColor transparent transparent;
|
||||
&::after {
|
||||
border-color: currentColor currentColor transparent transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.@{ribbon-prefix-cls}-placement-start {
|
||||
right: -8px;
|
||||
left: unset;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: @border-radius-sm;
|
||||
.@{ribbon-prefix-cls}-corner {
|
||||
right: 0;
|
||||
left: unset;
|
||||
border-color: currentColor transparent transparent currentColor;
|
||||
&::after {
|
||||
border-color: currentColor transparent transparent currentColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes antZoomBadgeInRtl {
|
||||
0% {
|
||||
transform: scale(0) translate(-50%, -50%);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1) translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes antZoomBadgeOutRtl {
|
||||
0% {
|
||||
transform: scale(1) translate(-50%, -50%);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0) translate(-50%, -50%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { PresetColorTypes } from '../_util/colors';
|
||||
|
||||
export function isPresetColor(color?: string): boolean {
|
||||
return (PresetColorTypes as string[]).indexOf(color) !== -1;
|
||||
return (PresetColorTypes as any[]).indexOf(color) !== -1;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { defineComponent, inject } from 'vue';
|
||||
import { defineComponent, ExtractPropTypes } from 'vue';
|
||||
import PropsTypes from '../_util/vue-types';
|
||||
import { getComponent, getSlot } from '../_util/props-util';
|
||||
import { defaultConfigProvider } from '../config-provider';
|
||||
import { flattenChildren } from '../_util/props-util';
|
||||
import { VueNode, withInstall } from '../_util/type';
|
||||
export const CommentProps = {
|
||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||
export const commentProps = {
|
||||
actions: PropsTypes.array,
|
||||
/** The element to display as the comment author. */
|
||||
author: PropsTypes.VNodeChild,
|
||||
|
@ -17,79 +17,79 @@ export const CommentProps = {
|
|||
datetime: PropsTypes.VNodeChild,
|
||||
};
|
||||
|
||||
export type CommentProps = Partial<ExtractPropTypes<typeof commentProps>>;
|
||||
|
||||
const Comment = defineComponent({
|
||||
name: 'AComment',
|
||||
props: CommentProps,
|
||||
setup() {
|
||||
return {
|
||||
configProvider: inject('configProvider', defaultConfigProvider),
|
||||
props: commentProps,
|
||||
slots: ['actions', 'author', 'avatar', 'content', 'datetime'],
|
||||
setup(props, { slots }) {
|
||||
const { prefixCls, direction } = useConfigInject('comment', props);
|
||||
const renderNested = (prefixCls: string, children: VueNode) => {
|
||||
return <div class={`${prefixCls}-nested`}>{children}</div>;
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getAction(actions: VueNode[]) {
|
||||
const getAction = (actions: VueNode[]) => {
|
||||
if (!actions || !actions.length) {
|
||||
return null;
|
||||
}
|
||||
const actionList = actions.map((action, index) => <li key={`action-${index}`}>{action}</li>);
|
||||
return actionList;
|
||||
},
|
||||
renderNested(prefixCls: string, children: VueNode) {
|
||||
return <div class={`${prefixCls}-nested`}>{children}</div>;
|
||||
},
|
||||
},
|
||||
};
|
||||
return () => {
|
||||
const pre = prefixCls.value;
|
||||
|
||||
render() {
|
||||
const { prefixCls: customizePrefixCls } = this.$props;
|
||||
const actions = props.actions ?? slots.actions?.();
|
||||
const author = props.author ?? slots.author?.();
|
||||
const avatar = props.avatar ?? slots.avatar?.();
|
||||
const content = props.content ?? slots.content?.();
|
||||
const datetime = props.datetime ?? slots.datetime?.();
|
||||
|
||||
const getPrefixCls = this.configProvider.getPrefixCls;
|
||||
const prefixCls = getPrefixCls('comment', customizePrefixCls);
|
||||
const avatarDom = (
|
||||
<div class={`${pre}-avatar`}>
|
||||
{typeof avatar === 'string' ? <img src={avatar} alt="comment-avatar" /> : avatar}
|
||||
</div>
|
||||
);
|
||||
|
||||
const actions = getComponent(this, 'actions');
|
||||
const author = getComponent(this, 'author');
|
||||
const avatar = getComponent(this, 'avatar');
|
||||
const content = getComponent(this, 'content');
|
||||
const datetime = getComponent(this, 'datetime');
|
||||
const actionDom = actions ? (
|
||||
<ul class={`${pre}-actions`}>{getAction(Array.isArray(actions) ? actions : [actions])}</ul>
|
||||
) : null;
|
||||
|
||||
const avatarDom = (
|
||||
<div class={`${prefixCls}-avatar`}>
|
||||
{typeof avatar === 'string' ? <img src={avatar} alt="comment-avatar" /> : avatar}
|
||||
</div>
|
||||
);
|
||||
const authorContent = (
|
||||
<div class={`${pre}-content-author`}>
|
||||
{author && <span class={`${pre}-content-author-name`}>{author}</span>}
|
||||
{datetime && <span class={`${pre}-content-author-time`}>{datetime}</span>}
|
||||
</div>
|
||||
);
|
||||
|
||||
const actionDom = actions ? (
|
||||
<ul class={`${prefixCls}-actions`}>
|
||||
{this.getAction(Array.isArray(actions) ? actions : [actions])}
|
||||
</ul>
|
||||
) : null;
|
||||
const contentDom = (
|
||||
<div class={`${pre}-content`}>
|
||||
{authorContent}
|
||||
<div class={`${pre}-content-detail`}>{content}</div>
|
||||
{actionDom}
|
||||
</div>
|
||||
);
|
||||
|
||||
const authorContent = (
|
||||
<div class={`${prefixCls}-content-author`}>
|
||||
{author && <span class={`${prefixCls}-content-author-name`}>{author}</span>}
|
||||
{datetime && <span class={`${prefixCls}-content-author-time`}>{datetime}</span>}
|
||||
</div>
|
||||
);
|
||||
|
||||
const contentDom = (
|
||||
<div class={`${prefixCls}-content`}>
|
||||
{authorContent}
|
||||
<div class={`${prefixCls}-content-detail`}>{content}</div>
|
||||
{actionDom}
|
||||
</div>
|
||||
);
|
||||
|
||||
const comment = (
|
||||
<div class={`${prefixCls}-inner`}>
|
||||
{avatarDom}
|
||||
{contentDom}
|
||||
</div>
|
||||
);
|
||||
const children = getSlot(this);
|
||||
return (
|
||||
<div class={prefixCls}>
|
||||
{comment}
|
||||
{children && children.length ? this.renderNested(prefixCls, children) : null}
|
||||
</div>
|
||||
);
|
||||
const comment = (
|
||||
<div class={`${pre}-inner`}>
|
||||
{avatarDom}
|
||||
{contentDom}
|
||||
</div>
|
||||
);
|
||||
const children = flattenChildren(slots.default?.());
|
||||
return (
|
||||
<div
|
||||
class={[
|
||||
pre,
|
||||
{
|
||||
[`${pre}-rtl`]: direction.value === 'rtl',
|
||||
},
|
||||
]}
|
||||
>
|
||||
{comment}
|
||||
{children && children.length ? renderNested(pre, children) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -15,8 +15,9 @@
|
|||
&-avatar {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
margin-right: 12px;
|
||||
margin-right: @margin-sm;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
@ -35,11 +36,11 @@
|
|||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: @margin-xss;
|
||||
font-size: @comment-font-size-base;
|
||||
& > a,
|
||||
& > span {
|
||||
padding-right: 8px;
|
||||
padding-right: @padding-xs;
|
||||
font-size: @comment-font-size-sm;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
@ -64,23 +65,27 @@
|
|||
}
|
||||
|
||||
&-detail p {
|
||||
margin-bottom: @comment-content-detail-p-margin-bottom;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
&-actions {
|
||||
margin-top: 12px;
|
||||
margin-top: @comment-actions-margin-top;
|
||||
margin-bottom: @comment-actions-margin-bottom;
|
||||
padding-left: 0;
|
||||
|
||||
> li {
|
||||
display: inline-block;
|
||||
color: @comment-action-color;
|
||||
> span {
|
||||
padding-right: 10px;
|
||||
margin-right: 10px;
|
||||
color: @comment-action-color;
|
||||
font-size: @comment-font-size-sm;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
color: @comment-action-hover-color;
|
||||
}
|
||||
|
@ -92,3 +97,5 @@
|
|||
margin-left: @comment-nest-indent;
|
||||
}
|
||||
}
|
||||
|
||||
@import './rtl';
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
@import '../../style/themes/index';
|
||||
@import '../../style/mixins/index';
|
||||
|
||||
@comment-prefix-cls: ~'@{ant-prefix}-comment';
|
||||
|
||||
.@{comment-prefix-cls} {
|
||||
&-rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
&-avatar {
|
||||
.@{comment-prefix-cls}-rtl & {
|
||||
margin-right: 0;
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
&-author {
|
||||
& > a,
|
||||
& > span {
|
||||
.@{comment-prefix-cls}-rtl & {
|
||||
padding-right: 0;
|
||||
padding-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-actions {
|
||||
.@{comment-prefix-cls}-rtl & {
|
||||
padding-right: 0;
|
||||
}
|
||||
> li {
|
||||
> span {
|
||||
.@{comment-prefix-cls}-rtl & {
|
||||
margin-right: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-nested {
|
||||
.@{comment-prefix-cls}-rtl & {
|
||||
margin-right: @comment-nest-indent;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
import { defineComponent, PropType, provide } from 'vue';
|
||||
|
||||
export type SizeType = 'small' | 'middle' | 'large' | undefined;
|
||||
|
||||
export const SizeContextProvider = defineComponent({
|
||||
props: {
|
||||
size: String as PropType<SizeType>,
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
provide('sizeProvider', props.size);
|
||||
|
||||
return () => slots.default?.();
|
||||
},
|
||||
});
|
|
@ -1,10 +1,19 @@
|
|||
import { reactive, provide, VNodeTypes, PropType, defineComponent, watch } from 'vue';
|
||||
import {
|
||||
reactive,
|
||||
provide,
|
||||
PropType,
|
||||
defineComponent,
|
||||
watch,
|
||||
ExtractPropTypes,
|
||||
UnwrapRef,
|
||||
} from 'vue';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import defaultRenderEmpty, { RenderEmptyHandler } from './renderEmpty';
|
||||
import LocaleProvider, { Locale, ANT_MARK } from '../locale-provider';
|
||||
import { TransformCellTextProps } from '../table/interface';
|
||||
import LocaleReceiver from '../locale-provider/LocaleReceiver';
|
||||
import { withInstall } from '../_util/type';
|
||||
import { RequiredMark } from '../form/Form';
|
||||
|
||||
export type SizeType = 'small' | 'middle' | 'large' | undefined;
|
||||
|
||||
|
@ -14,6 +23,8 @@ export interface CSPConfig {
|
|||
|
||||
export { RenderEmptyHandler };
|
||||
|
||||
export type Direction = 'ltr' | 'rtl';
|
||||
|
||||
export interface ConfigConsumerProps {
|
||||
getTargetContainer?: () => HTMLElement;
|
||||
getPopupContainer?: (triggerNode: HTMLElement) => HTMLElement;
|
||||
|
@ -30,6 +41,7 @@ export interface ConfigConsumerProps {
|
|||
pageHeader?: {
|
||||
ghost: boolean;
|
||||
};
|
||||
componentSize?: SizeType;
|
||||
direction?: 'ltr' | 'rtl';
|
||||
space?: {
|
||||
size?: SizeType | number;
|
||||
|
@ -50,72 +62,54 @@ export const configConsumerProps = [
|
|||
'pageHeader',
|
||||
];
|
||||
|
||||
export interface ConfigProviderProps {
|
||||
getTargetContainer?: () => HTMLElement;
|
||||
getPopupContainer?: (triggerNode: HTMLElement) => HTMLElement;
|
||||
prefixCls?: string;
|
||||
children?: VNodeTypes;
|
||||
renderEmpty?: RenderEmptyHandler;
|
||||
transformCellText?: (tableProps: TransformCellTextProps) => any;
|
||||
csp?: CSPConfig;
|
||||
autoInsertSpaceInButton?: boolean;
|
||||
input?: {
|
||||
autoComplete?: string;
|
||||
};
|
||||
locale?: Locale;
|
||||
pageHeader?: {
|
||||
ghost: boolean;
|
||||
};
|
||||
componentSize?: SizeType;
|
||||
direction?: 'ltr' | 'rtl';
|
||||
space?: {
|
||||
size?: SizeType | number;
|
||||
};
|
||||
virtual?: boolean;
|
||||
dropdownMatchSelectWidth?: boolean;
|
||||
}
|
||||
export const configProviderProps = {
|
||||
getTargetContainer: {
|
||||
type: Function as PropType<() => HTMLElement>,
|
||||
},
|
||||
getPopupContainer: {
|
||||
type: Function as PropType<(triggerNode: HTMLElement) => HTMLElement>,
|
||||
},
|
||||
prefixCls: String,
|
||||
getPrefixCls: {
|
||||
type: Function as PropType<(suffixCls?: string, customizePrefixCls?: string) => string>,
|
||||
},
|
||||
renderEmpty: {
|
||||
type: Function as PropType<RenderEmptyHandler>,
|
||||
},
|
||||
transformCellText: {
|
||||
type: Function as PropType<(tableProps: TransformCellTextProps) => any>,
|
||||
},
|
||||
csp: {
|
||||
type: Object as PropType<CSPConfig>,
|
||||
},
|
||||
autoInsertSpaceInButton: PropTypes.looseBool,
|
||||
locale: {
|
||||
type: Object as PropType<Locale>,
|
||||
},
|
||||
pageHeader: {
|
||||
type: Object as PropType<{ ghost: boolean }>,
|
||||
},
|
||||
componentSize: {
|
||||
type: String as PropType<SizeType>,
|
||||
},
|
||||
direction: {
|
||||
type: String as PropType<'ltr' | 'rtl'>,
|
||||
},
|
||||
space: {
|
||||
type: Object as PropType<{ size: SizeType | number }>,
|
||||
},
|
||||
virtual: PropTypes.looseBool,
|
||||
dropdownMatchSelectWidth: PropTypes.looseBool,
|
||||
form: {
|
||||
type: Object as PropType<{ requiredMark?: RequiredMark }>,
|
||||
},
|
||||
};
|
||||
|
||||
export type ConfigProviderProps = Partial<ExtractPropTypes<typeof configProviderProps>>;
|
||||
|
||||
const ConfigProvider = defineComponent({
|
||||
name: 'AConfigProvider',
|
||||
props: {
|
||||
getTargetContainer: {
|
||||
type: Function as PropType<() => HTMLElement>,
|
||||
},
|
||||
getPopupContainer: {
|
||||
type: Function as PropType<(triggerNode: HTMLElement) => HTMLElement>,
|
||||
},
|
||||
prefixCls: String,
|
||||
getPrefixCls: {
|
||||
type: Function as PropType<(suffixCls?: string, customizePrefixCls?: string) => string>,
|
||||
},
|
||||
renderEmpty: {
|
||||
type: Function as PropType<RenderEmptyHandler>,
|
||||
},
|
||||
transformCellText: {
|
||||
type: Function as PropType<(tableProps: TransformCellTextProps) => any>,
|
||||
},
|
||||
csp: {
|
||||
type: Object as PropType<CSPConfig>,
|
||||
},
|
||||
autoInsertSpaceInButton: PropTypes.looseBool,
|
||||
locale: {
|
||||
type: Object as PropType<Locale>,
|
||||
},
|
||||
pageHeader: {
|
||||
type: Object as PropType<{ ghost: boolean }>,
|
||||
},
|
||||
componentSize: {
|
||||
type: Object as PropType<SizeType>,
|
||||
},
|
||||
direction: {
|
||||
type: String as PropType<'ltr' | 'rtl'>,
|
||||
},
|
||||
space: {
|
||||
type: [String, Number] as PropType<SizeType | number>,
|
||||
},
|
||||
virtual: PropTypes.looseBool,
|
||||
dropdownMatchSelectWidth: PropTypes.looseBool,
|
||||
},
|
||||
props: configProviderProps,
|
||||
setup(props, { slots }) {
|
||||
const getPrefixCls = (suffixCls?: string, customizePrefixCls?: string) => {
|
||||
const { prefixCls = 'ant' } = props;
|
||||
|
@ -166,12 +160,13 @@ const ConfigProvider = defineComponent({
|
|||
},
|
||||
});
|
||||
|
||||
export const defaultConfigProvider: ConfigConsumerProps = {
|
||||
export const defaultConfigProvider: UnwrapRef<ConfigProviderProps> = reactive({
|
||||
getPrefixCls: (suffixCls: string, customizePrefixCls?: string) => {
|
||||
if (customizePrefixCls) return customizePrefixCls;
|
||||
return `ant-${suffixCls}`;
|
||||
return suffixCls ? `ant-${suffixCls}` : 'ant';
|
||||
},
|
||||
renderEmpty: defaultRenderEmpty,
|
||||
};
|
||||
direction: 'ltr',
|
||||
});
|
||||
|
||||
export default withInstall(ConfigProvider);
|
||||
|
|
|
@ -1,46 +1,67 @@
|
|||
import { flattenChildren } from '../_util/props-util';
|
||||
import { computed, defineComponent, inject, PropType } from 'vue';
|
||||
import { computed, defineComponent, ExtractPropTypes, inject, PropType } from 'vue';
|
||||
import { defaultConfigProvider } from '../config-provider';
|
||||
import { withInstall } from '../_util/type';
|
||||
|
||||
export const dividerProps = {
|
||||
prefixCls: String,
|
||||
type: {
|
||||
type: String as PropType<'horizontal' | 'vertical' | ''>,
|
||||
default: 'horizontal',
|
||||
},
|
||||
dashed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
orientation: {
|
||||
type: String as PropType<'left' | 'right' | 'center'>,
|
||||
default: 'center',
|
||||
},
|
||||
plain: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
export type DividerProps = Partial<ExtractPropTypes<typeof dividerProps>>;
|
||||
|
||||
const Divider = defineComponent({
|
||||
name: 'ADivider',
|
||||
props: {
|
||||
prefixCls: String,
|
||||
type: {
|
||||
type: String as PropType<'horizontal' | 'vertical' | ''>,
|
||||
default: 'horizontal',
|
||||
},
|
||||
dashed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
orientation: {
|
||||
type: String as PropType<'left' | 'right' | 'center'>,
|
||||
default: 'center',
|
||||
},
|
||||
},
|
||||
props: dividerProps,
|
||||
setup(props, { slots }) {
|
||||
const { getPrefixCls } = inject('configProvider', defaultConfigProvider);
|
||||
const prefixCls = computed(() => getPrefixCls('divider', props.prefixCls));
|
||||
const configProvider = inject('configProvider', defaultConfigProvider);
|
||||
const prefixClsRef = computed(() => configProvider.getPrefixCls('divider', props.prefixCls));
|
||||
|
||||
const classString = computed(() => {
|
||||
const { type, dashed, orientation } = props;
|
||||
const orientationPrefix = orientation.length > 0 ? '-' + orientation : orientation;
|
||||
const prefixClsRef = prefixCls.value;
|
||||
const { type, dashed, plain } = props;
|
||||
const prefixCls = prefixClsRef.value;
|
||||
return {
|
||||
[prefixClsRef]: true,
|
||||
[`${prefixClsRef}-${type}`]: true,
|
||||
[`${prefixClsRef}-with-text${orientationPrefix}`]: slots.default,
|
||||
[`${prefixClsRef}-dashed`]: !!dashed,
|
||||
[prefixCls]: true,
|
||||
[`${prefixCls}-${type}`]: true,
|
||||
[`${prefixCls}-dashed`]: !!dashed,
|
||||
[`${prefixCls}-plain`]: !!plain,
|
||||
[`${prefixCls}-rtl`]: configProvider.direction === 'rtl',
|
||||
};
|
||||
});
|
||||
|
||||
const orientationPrefix = computed(() =>
|
||||
props.orientation.length > 0 ? '-' + props.orientation : props.orientation,
|
||||
);
|
||||
|
||||
return () => {
|
||||
const children = flattenChildren(slots.default?.());
|
||||
return (
|
||||
<div class={classString.value} role="separator">
|
||||
{children.length ? <span class={`${prefixCls.value}-inner-text`}>{children}</span> : null}
|
||||
<div
|
||||
class={[
|
||||
classString.value,
|
||||
children.length
|
||||
? `${prefixClsRef.value}-with-text ${prefixClsRef.value}-with-text${orientationPrefix.value}`
|
||||
: '',
|
||||
]}
|
||||
role="separator"
|
||||
>
|
||||
{children.length ? (
|
||||
<span class={`${prefixClsRef.value}-inner-text`}>{children}</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,59 +6,52 @@
|
|||
.@{divider-prefix-cls} {
|
||||
.reset-component();
|
||||
|
||||
background: @border-color-split;
|
||||
border-top: @border-width-base solid @divider-color;
|
||||
|
||||
&, /* for compatiable */
|
||||
&-vertical {
|
||||
position: relative;
|
||||
top: -0.06em;
|
||||
display: inline-block;
|
||||
width: 1px;
|
||||
height: 0.9em;
|
||||
margin: 0 8px;
|
||||
vertical-align: middle;
|
||||
border-top: 0;
|
||||
border-left: @border-width-base solid @divider-color;
|
||||
}
|
||||
|
||||
&-horizontal {
|
||||
display: block;
|
||||
display: flex;
|
||||
clear: both;
|
||||
width: 100%;
|
||||
min-width: 100%; // Fix https://github.com/ant-design/ant-design/issues/10914
|
||||
height: 1px;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
&-horizontal&-with-text-center,
|
||||
&-horizontal&-with-text-left,
|
||||
&-horizontal&-with-text-right {
|
||||
display: table;
|
||||
&-horizontal&-with-text {
|
||||
display: flex;
|
||||
margin: 16px 0;
|
||||
color: @heading-color;
|
||||
font-weight: 500;
|
||||
font-size: @font-size-lg;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
background: transparent;
|
||||
border-top: 0;
|
||||
border-top-color: @divider-color;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
display: table-cell;
|
||||
width: 50%;
|
||||
border-top: 1px solid @border-color-split;
|
||||
border-top: @border-width-base solid transparent;
|
||||
// Chrome not accept `inherit` in `border-top`
|
||||
border-top-color: inherit;
|
||||
border-bottom: 0;
|
||||
transform: translateY(50%);
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
&-horizontal&-with-text-left,
|
||||
&-horizontal&-with-text-right {
|
||||
.@{divider-prefix-cls}-inner-text {
|
||||
display: inline-block;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&-horizontal&-with-text-left {
|
||||
&::before {
|
||||
top: 50%;
|
||||
|
@ -90,12 +83,10 @@
|
|||
background: none;
|
||||
border-color: @divider-color;
|
||||
border-style: dashed;
|
||||
border-width: 1px 0 0;
|
||||
border-width: @border-width-base 0 0;
|
||||
}
|
||||
|
||||
&-horizontal&-with-text-center&-dashed,
|
||||
&-horizontal&-with-text-left&-dashed,
|
||||
&-horizontal&-with-text-right&-dashed {
|
||||
&-horizontal&-with-text&-dashed {
|
||||
border-top: 0;
|
||||
&::before,
|
||||
&::after {
|
||||
|
@ -104,6 +95,14 @@
|
|||
}
|
||||
|
||||
&-vertical&-dashed {
|
||||
border-width: 0 0 0 1px;
|
||||
border-width: 0 0 0 @border-width-base;
|
||||
}
|
||||
|
||||
&-plain&-with-text {
|
||||
color: @text-color;
|
||||
font-weight: normal;
|
||||
font-size: @font-size-base;
|
||||
}
|
||||
}
|
||||
|
||||
@import './rtl';
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
@import '../../style/themes/index';
|
||||
@import '../../style/mixins/index';
|
||||
|
||||
@divider-prefix-cls: ~'@{ant-prefix}-divider';
|
||||
|
||||
.@{divider-prefix-cls} {
|
||||
&-rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
&-horizontal&-with-text-left {
|
||||
&::before {
|
||||
.@{divider-prefix-cls}-rtl& {
|
||||
width: 100% - @divider-orientation-margin;
|
||||
}
|
||||
}
|
||||
&::after {
|
||||
.@{divider-prefix-cls}-rtl& {
|
||||
width: @divider-orientation-margin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-horizontal&-with-text-right {
|
||||
&::before {
|
||||
.@{divider-prefix-cls}-rtl& {
|
||||
width: @divider-orientation-margin;
|
||||
}
|
||||
}
|
||||
&::after {
|
||||
.@{divider-prefix-cls}-rtl& {
|
||||
width: 100% - @divider-orientation-margin;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
.@{empty-prefix-cls} {
|
||||
margin: 0 8px;
|
||||
font-size: @empty-font-size;
|
||||
line-height: 22px;
|
||||
line-height: @line-height-base;
|
||||
text-align: center;
|
||||
|
||||
&-image {
|
||||
|
@ -24,10 +24,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
&-description {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&-footer {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
@ -56,8 +52,8 @@
|
|||
// not support the definition because the less variables have no meaning
|
||||
& when (@theme = dark) {
|
||||
&-ellipse {
|
||||
fill-opacity: 0.08;
|
||||
fill: @white;
|
||||
fill-opacity: 0.08;
|
||||
}
|
||||
&-path {
|
||||
&-1 {
|
||||
|
@ -82,8 +78,8 @@
|
|||
}
|
||||
& when not (@theme = dark) {
|
||||
&-ellipse {
|
||||
fill-opacity: 0.8;
|
||||
fill: #f5f5f5;
|
||||
fill-opacity: 0.8;
|
||||
}
|
||||
&-path {
|
||||
&-1 {
|
||||
|
@ -135,3 +131,5 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@import './rtl';
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
@import '../../style/themes/index';
|
||||
@import '../../style/mixins/index';
|
||||
|
||||
@empty-prefix-cls: ~'@{ant-prefix}-empty';
|
||||
|
||||
.@{empty-prefix-cls} {
|
||||
&-rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
import { useInjectFormItemPrefix } from './context';
|
||||
import { VueNode } from '../_util/type';
|
||||
import { defineComponent, onBeforeUnmount, ref, watch } from '@vue/runtime-core';
|
||||
import classNames from '../_util/classNames';
|
||||
import Transition, { getTransitionProps } from '../_util/transition';
|
||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||
|
||||
export interface ErrorListProps {
|
||||
errors?: VueNode[];
|
||||
/** @private Internal Usage. Do not use in your production */
|
||||
help?: VueNode;
|
||||
/** @private Internal Usage. Do not use in your production */
|
||||
onDomErrorVisibleChange?: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ErrorList',
|
||||
props: ['errors', 'help', 'onDomErrorVisibleChange'],
|
||||
setup(props) {
|
||||
const { prefixCls: rootPrefixCls } = useConfigInject('', props);
|
||||
const { prefixCls, status } = useInjectFormItemPrefix();
|
||||
const visible = ref(!!(props.errors && props.errors.length));
|
||||
const innerStatus = ref(status.value);
|
||||
let timeout = ref();
|
||||
const cacheErrors = ref([...props.errors]);
|
||||
watch([() => [...props.errors], () => props.help], newValues => {
|
||||
window.clearTimeout(timeout.value);
|
||||
if (props.help) {
|
||||
visible.value = !!(props.errors && props.errors.length);
|
||||
if (visible.value) {
|
||||
cacheErrors.value = newValues[0];
|
||||
}
|
||||
} else {
|
||||
timeout.value = window.setTimeout(() => {
|
||||
visible.value = !!(props.errors && props.errors.length);
|
||||
if (visible.value) {
|
||||
cacheErrors.value = newValues[0];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
window.clearTimeout(timeout.value);
|
||||
});
|
||||
// Memo status in same visible
|
||||
watch([visible, status], () => {
|
||||
if (visible.value && status.value) {
|
||||
innerStatus.value = status.value;
|
||||
}
|
||||
});
|
||||
watch(
|
||||
visible,
|
||||
() => {
|
||||
if (visible.value) {
|
||||
props.onDomErrorVisibleChange?.(true);
|
||||
}
|
||||
},
|
||||
{ immediate: true, flush: 'post' },
|
||||
);
|
||||
return () => {
|
||||
const baseClassName = `${prefixCls.value}-item-explain`;
|
||||
const transitionProps = getTransitionProps(`${rootPrefixCls.value}-show-help`, {
|
||||
onAfterLeave: () => {
|
||||
props.onDomErrorVisibleChange?.(false);
|
||||
},
|
||||
});
|
||||
return (
|
||||
<Transition {...transitionProps}>
|
||||
{visible.value ? (
|
||||
<div
|
||||
class={classNames(baseClassName, {
|
||||
[`${baseClassName}-${innerStatus.value}`]: innerStatus.value,
|
||||
})}
|
||||
key="help"
|
||||
>
|
||||
{cacheErrors.value?.map((error: any, index: number) => (
|
||||
<div key={index} role="alert">
|
||||
{error}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -1,10 +1,16 @@
|
|||
import { defineComponent, inject, provide, PropType, computed, ExtractPropTypes } from 'vue';
|
||||
import {
|
||||
defineComponent,
|
||||
PropType,
|
||||
computed,
|
||||
ExtractPropTypes,
|
||||
HTMLAttributes,
|
||||
watch,
|
||||
ref,
|
||||
} from 'vue';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import classNames from '../_util/classNames';
|
||||
import warning from '../_util/warning';
|
||||
import FormItem from './FormItem';
|
||||
import { getSlot } from '../_util/props-util';
|
||||
import { defaultConfigProvider } from '../config-provider';
|
||||
import FormItem, { FieldExpose } from './FormItem';
|
||||
import { getNamePath, containsNamePath } from './utils/valueUtil';
|
||||
import { defaultValidateMessages } from './utils/messages';
|
||||
import { allPromiseFinish } from './utils/asyncUtil';
|
||||
|
@ -15,6 +21,13 @@ import initDefaultProps from '../_util/props-util/initDefaultProps';
|
|||
import { tuple, VueNode } from '../_util/type';
|
||||
import { ColProps } from '../grid/Col';
|
||||
import { InternalNamePath, NamePath, ValidateErrorEntity, ValidateOptions } from './interface';
|
||||
import { useInjectSize } from '../_util/hooks/useSize';
|
||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||
import { useProvideForm } from './context';
|
||||
import { SizeType } from '../config-provider';
|
||||
|
||||
export type RequiredMark = boolean | 'optional';
|
||||
export type FormLayout = 'horizontal' | 'inline' | 'vertical';
|
||||
|
||||
export type ValidationRule = {
|
||||
/** validation error message */
|
||||
|
@ -45,11 +58,13 @@ export type ValidationRule = {
|
|||
|
||||
export const formProps = {
|
||||
layout: PropTypes.oneOf(tuple('horizontal', 'inline', 'vertical')),
|
||||
labelCol: { type: Object as PropType<ColProps> },
|
||||
wrapperCol: { type: Object as PropType<ColProps> },
|
||||
labelCol: { type: Object as PropType<ColProps & HTMLAttributes> },
|
||||
wrapperCol: { type: Object as PropType<ColProps & HTMLAttributes> },
|
||||
colon: PropTypes.looseBool,
|
||||
labelAlign: PropTypes.oneOf(tuple('left', 'right')),
|
||||
prefixCls: PropTypes.string,
|
||||
requiredMark: { type: [String, Boolean] as PropType<RequiredMark | ''>, default: undefined },
|
||||
/** @deprecated Will warning in future branch. Pls use `requiredMark` instead. */
|
||||
hideRequiredMark: PropTypes.looseBool,
|
||||
model: PropTypes.object,
|
||||
rules: { type: Object as PropType<{ [k: string]: ValidationRule[] | ValidationRule }> },
|
||||
|
@ -62,6 +77,7 @@ export const formProps = {
|
|||
onFinishFailed: PropTypes.func,
|
||||
name: PropTypes.string,
|
||||
validateTrigger: { type: [String, Array] as PropType<string | string[]> },
|
||||
size: { type: String as PropType<SizeType> },
|
||||
};
|
||||
|
||||
export type FormProps = Partial<ExtractPropTypes<typeof formProps>>;
|
||||
|
@ -79,92 +95,88 @@ const Form = defineComponent({
|
|||
colon: true,
|
||||
}),
|
||||
Item: FormItem,
|
||||
setup(props) {
|
||||
return {
|
||||
configProvider: inject('configProvider', defaultConfigProvider),
|
||||
fields: [],
|
||||
form: undefined,
|
||||
lastValidatePromise: null,
|
||||
vertical: computed(() => props.layout === 'vertical'),
|
||||
emits: ['finishFailed', 'submit', 'finish'],
|
||||
setup(props, { emit, slots, expose }) {
|
||||
const size = useInjectSize(props);
|
||||
const { prefixCls, direction, form: contextForm } = useConfigInject('form', props);
|
||||
const requiredMark = computed(() => props.requiredMark === '' || props.requiredMark);
|
||||
const mergedRequiredMark = computed(() => {
|
||||
if (requiredMark.value !== undefined) {
|
||||
return requiredMark.value;
|
||||
}
|
||||
|
||||
if (contextForm && contextForm.value?.requiredMark !== undefined) {
|
||||
return contextForm.value.requiredMark;
|
||||
}
|
||||
|
||||
if (props.hideRequiredMark) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const formClassName = computed(() =>
|
||||
classNames(prefixCls.value, {
|
||||
[`${prefixCls.value}-${props.layout}`]: true,
|
||||
[`${prefixCls.value}-hide-required-mark`]: mergedRequiredMark.value === false,
|
||||
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
|
||||
[`${prefixCls.value}-${size.value}`]: size.value,
|
||||
}),
|
||||
);
|
||||
const lastValidatePromise = ref();
|
||||
const fields: Record<string, FieldExpose> = {};
|
||||
|
||||
const addField = (eventKey: string, field: FieldExpose) => {
|
||||
fields[eventKey] = field;
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
rules() {
|
||||
if (this.validateOnRuleChange) {
|
||||
this.validateFields();
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
provide('FormContext', this);
|
||||
},
|
||||
methods: {
|
||||
addField(field: any) {
|
||||
if (field) {
|
||||
this.fields.push(field);
|
||||
}
|
||||
},
|
||||
removeField(field: any) {
|
||||
if (field.fieldName) {
|
||||
this.fields.splice(this.fields.indexOf(field), 1);
|
||||
}
|
||||
},
|
||||
handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.$emit('submit', e);
|
||||
const res = this.validateFields();
|
||||
res
|
||||
.then(values => {
|
||||
this.$emit('finish', values);
|
||||
})
|
||||
.catch(errors => {
|
||||
this.handleFinishFailed(errors);
|
||||
});
|
||||
},
|
||||
getFieldsByNameList(nameList: NamePath) {
|
||||
const removeField = (eventKey: string) => {
|
||||
delete fields[eventKey];
|
||||
};
|
||||
|
||||
const getFieldsByNameList = (nameList: NamePath) => {
|
||||
const provideNameList = !!nameList;
|
||||
const namePathList = provideNameList ? toArray(nameList).map(getNamePath) : [];
|
||||
if (!provideNameList) {
|
||||
return this.fields;
|
||||
return Object.values(fields);
|
||||
} else {
|
||||
return this.fields.filter(
|
||||
field => namePathList.findIndex(namePath => isEqualName(namePath, field.fieldName)) > -1,
|
||||
return Object.values(fields).filter(
|
||||
field =>
|
||||
namePathList.findIndex(namePath => isEqualName(namePath, field.fieldName.value)) > -1,
|
||||
);
|
||||
}
|
||||
},
|
||||
resetFields(name: NamePath) {
|
||||
if (!this.model) {
|
||||
};
|
||||
const resetFields = (name: NamePath) => {
|
||||
if (!props.model) {
|
||||
warning(false, 'Form', 'model is required for resetFields to work.');
|
||||
return;
|
||||
}
|
||||
this.getFieldsByNameList(name).forEach(field => {
|
||||
getFieldsByNameList(name).forEach(field => {
|
||||
field.resetField();
|
||||
});
|
||||
},
|
||||
clearValidate(name: NamePath) {
|
||||
this.getFieldsByNameList(name).forEach(field => {
|
||||
};
|
||||
const clearValidate = (name: NamePath) => {
|
||||
getFieldsByNameList(name).forEach(field => {
|
||||
field.clearValidate();
|
||||
});
|
||||
},
|
||||
handleFinishFailed(errorInfo: ValidateErrorEntity) {
|
||||
const { scrollToFirstError } = this;
|
||||
this.$emit('finishFailed', errorInfo);
|
||||
};
|
||||
const handleFinishFailed = (errorInfo: ValidateErrorEntity) => {
|
||||
const { scrollToFirstError } = props;
|
||||
emit('finishFailed', errorInfo);
|
||||
if (scrollToFirstError && errorInfo.errorFields.length) {
|
||||
let scrollToFieldOptions: Options = {};
|
||||
if (typeof scrollToFirstError === 'object') {
|
||||
scrollToFieldOptions = scrollToFirstError;
|
||||
}
|
||||
this.scrollToField(errorInfo.errorFields[0].name, scrollToFieldOptions);
|
||||
scrollToField(errorInfo.errorFields[0].name, scrollToFieldOptions);
|
||||
}
|
||||
},
|
||||
validate(...args: any[]) {
|
||||
return this.validateField(...args);
|
||||
},
|
||||
scrollToField(name: NamePath, options = {}) {
|
||||
const fields = this.getFieldsByNameList(name);
|
||||
};
|
||||
const validate = (...args: any[]) => {
|
||||
return validateField(...args);
|
||||
};
|
||||
const scrollToField = (name: NamePath, options = {}) => {
|
||||
const fields = getFieldsByNameList(name);
|
||||
if (fields.length) {
|
||||
const fieldId = fields[0].fieldId;
|
||||
const fieldId = fields[0].fieldId.value;
|
||||
const node = fieldId ? document.getElementById(fieldId) : null;
|
||||
|
||||
if (node) {
|
||||
|
@ -175,12 +187,12 @@ const Form = defineComponent({
|
|||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
getFieldsValue(nameList: NamePath[] | true = true) {
|
||||
const getFieldsValue = (nameList: NamePath[] | true = true) => {
|
||||
const values: any = {};
|
||||
this.fields.forEach(({ fieldName, fieldValue }) => {
|
||||
values[fieldName] = fieldValue;
|
||||
Object.values(fields).forEach(({ fieldName, fieldValue }) => {
|
||||
values[fieldName.value] = fieldValue.value;
|
||||
});
|
||||
if (nameList === true) {
|
||||
return values;
|
||||
|
@ -191,14 +203,14 @@ const Form = defineComponent({
|
|||
);
|
||||
return res;
|
||||
}
|
||||
},
|
||||
validateFields(nameList?: NamePath[], options?: ValidateOptions) {
|
||||
};
|
||||
const validateFields = (nameList?: NamePath[], options?: ValidateOptions) => {
|
||||
warning(
|
||||
!(nameList instanceof Function),
|
||||
'Form',
|
||||
'validateFields/validateField/validate not support callback, please use promise instead',
|
||||
);
|
||||
if (!this.model) {
|
||||
if (!props.model) {
|
||||
warning(false, 'Form', 'model is required for validateFields to work.');
|
||||
return Promise.reject('Form `model` is required for validateFields to work.');
|
||||
}
|
||||
|
@ -213,25 +225,25 @@ const Form = defineComponent({
|
|||
errors: string[];
|
||||
}>[] = [];
|
||||
|
||||
this.fields.forEach(field => {
|
||||
Object.values(fields).forEach(field => {
|
||||
// Add field if not provide `nameList`
|
||||
if (!provideNameList) {
|
||||
namePathList.push(field.getNamePath());
|
||||
namePathList.push(field.namePath.value);
|
||||
}
|
||||
|
||||
// Skip if without rule
|
||||
if (!field.getRules().length) {
|
||||
if (!field.rules?.value.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fieldNamePath = field.getNamePath();
|
||||
const fieldNamePath = field.namePath.value;
|
||||
|
||||
// Add field validate rule in to promise list
|
||||
if (!provideNameList || containsNamePath(namePathList, fieldNamePath)) {
|
||||
const promise = field.validateRules({
|
||||
validateMessages: {
|
||||
...defaultValidateMessages,
|
||||
...this.validateMessages,
|
||||
...props.validateMessages,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
@ -251,21 +263,21 @@ const Form = defineComponent({
|
|||
});
|
||||
|
||||
const summaryPromise = allPromiseFinish(promiseList);
|
||||
this.lastValidatePromise = summaryPromise;
|
||||
lastValidatePromise.value = summaryPromise;
|
||||
|
||||
const returnPromise = summaryPromise
|
||||
.then(() => {
|
||||
if (this.lastValidatePromise === summaryPromise) {
|
||||
return Promise.resolve(this.getFieldsValue(namePathList));
|
||||
if (lastValidatePromise.value === summaryPromise) {
|
||||
return Promise.resolve(getFieldsValue(namePathList));
|
||||
}
|
||||
return Promise.reject([]);
|
||||
})
|
||||
.catch(results => {
|
||||
const errorList = results.filter(result => result && result.errors.length);
|
||||
return Promise.reject({
|
||||
values: this.getFieldsValue(namePathList),
|
||||
values: getFieldsValue(namePathList),
|
||||
errorFields: errorList,
|
||||
outOfDate: this.lastValidatePromise !== summaryPromise,
|
||||
outOfDate: lastValidatePromise.value !== summaryPromise,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -273,29 +285,65 @@ const Form = defineComponent({
|
|||
returnPromise.catch(e => e);
|
||||
|
||||
return returnPromise;
|
||||
},
|
||||
validateField(...args: any[]) {
|
||||
return this.validateFields(...args);
|
||||
},
|
||||
},
|
||||
};
|
||||
const validateField = (...args: any[]) => {
|
||||
return validateFields(...args);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { prefixCls: customizePrefixCls, hideRequiredMark, layout, handleSubmit } = this;
|
||||
const getPrefixCls = this.configProvider.getPrefixCls;
|
||||
const prefixCls = getPrefixCls('form', customizePrefixCls);
|
||||
const { class: className, ...restProps } = this.$attrs;
|
||||
const handleSubmit = (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
emit('submit', e);
|
||||
const res = validateFields();
|
||||
res
|
||||
.then(values => {
|
||||
emit('finish', values);
|
||||
})
|
||||
.catch(errors => {
|
||||
handleFinishFailed(errors);
|
||||
});
|
||||
};
|
||||
|
||||
const formClassName = classNames(prefixCls, className, {
|
||||
[`${prefixCls}-horizontal`]: layout === 'horizontal',
|
||||
[`${prefixCls}-vertical`]: layout === 'vertical',
|
||||
[`${prefixCls}-inline`]: layout === 'inline',
|
||||
[`${prefixCls}-hide-required-mark`]: hideRequiredMark,
|
||||
expose({
|
||||
resetFields,
|
||||
clearValidate,
|
||||
validateFields,
|
||||
getFieldsValue,
|
||||
validate,
|
||||
scrollToField,
|
||||
});
|
||||
return (
|
||||
<form onSubmit={handleSubmit} class={formClassName} {...restProps}>
|
||||
{getSlot(this)}
|
||||
</form>
|
||||
|
||||
useProvideForm({
|
||||
model: computed(() => props.model),
|
||||
name: computed(() => props.name),
|
||||
labelAlign: computed(() => props.labelAlign),
|
||||
labelCol: computed(() => props.labelCol),
|
||||
wrapperCol: computed(() => props.wrapperCol),
|
||||
vertical: computed(() => props.layout === 'vertical'),
|
||||
colon: computed(() => props.colon),
|
||||
requiredMark: mergedRequiredMark,
|
||||
validateTrigger: computed(() => props.validateTrigger),
|
||||
rules: computed(() => props.rules),
|
||||
addField,
|
||||
removeField,
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.rules,
|
||||
() => {
|
||||
if (props.validateOnRuleChange) {
|
||||
validateFields();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<form onSubmit={handleSubmit} class={formClassName.value}>
|
||||
{slots.default?.()}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,47 +1,47 @@
|
|||
import {
|
||||
inject,
|
||||
provide,
|
||||
PropType,
|
||||
defineComponent,
|
||||
computed,
|
||||
nextTick,
|
||||
ExtractPropTypes,
|
||||
ref,
|
||||
watchEffect,
|
||||
onBeforeUnmount,
|
||||
ComputedRef,
|
||||
} from 'vue';
|
||||
import cloneDeep from 'lodash-es/cloneDeep';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import classNames from '../_util/classNames';
|
||||
import { getTransitionProps, Transition } from '../_util/transition';
|
||||
import Row from '../grid/Row';
|
||||
import Col, { ColProps } from '../grid/Col';
|
||||
import hasProp, {
|
||||
findDOMNode,
|
||||
getComponent,
|
||||
getOptionProps,
|
||||
getEvents,
|
||||
isValidElement,
|
||||
getSlot,
|
||||
} from '../_util/props-util';
|
||||
import { ColProps } from '../grid/Col';
|
||||
import { isValidElement, flattenChildren, filterEmpty } from '../_util/props-util';
|
||||
import BaseMixin from '../_util/BaseMixin';
|
||||
import { defaultConfigProvider } from '../config-provider';
|
||||
import { cloneElement } from '../_util/vnode';
|
||||
import CheckCircleFilled from '@ant-design/icons-vue/CheckCircleFilled';
|
||||
import ExclamationCircleFilled from '@ant-design/icons-vue/ExclamationCircleFilled';
|
||||
import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled';
|
||||
import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined';
|
||||
import { validateRules } from './utils/validateUtil';
|
||||
import { validateRules as validateRulesUtil } from './utils/validateUtil';
|
||||
import { getNamePath } from './utils/valueUtil';
|
||||
import { toArray } from './utils/typeUtil';
|
||||
import { warning } from '../vc-util/warning';
|
||||
import find from 'lodash-es/find';
|
||||
import { tuple, VueNode } from '../_util/type';
|
||||
import { ValidateOptions } from './interface';
|
||||
import { tuple } from '../_util/type';
|
||||
import { InternalNamePath, RuleObject, ValidateOptions } from './interface';
|
||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||
import { useInjectForm } from './context';
|
||||
import FormItemLabel from './FormItemLabel';
|
||||
import FormItemInput from './FormItemInput';
|
||||
import { ValidationRule } from './Form';
|
||||
|
||||
const iconMap = {
|
||||
success: CheckCircleFilled,
|
||||
warning: ExclamationCircleFilled,
|
||||
error: CloseCircleFilled,
|
||||
validating: LoadingOutlined,
|
||||
};
|
||||
const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', '');
|
||||
export type ValidateStatus = typeof ValidateStatuses[number];
|
||||
|
||||
export interface FieldExpose {
|
||||
fieldValue: ComputedRef<any>;
|
||||
fieldId: ComputedRef<any>;
|
||||
fieldName: ComputedRef<any>;
|
||||
resetField: () => void;
|
||||
clearValidate: () => void;
|
||||
namePath: ComputedRef<InternalNamePath>;
|
||||
rules?: ComputedRef<ValidationRule[]>;
|
||||
validateRules: (options: ValidateOptions) => Promise<void> | Promise<string[]>;
|
||||
}
|
||||
|
||||
function getPropByPath(obj: any, namePathList: any, strict?: boolean) {
|
||||
let tempObj = obj;
|
||||
|
@ -95,19 +95,29 @@ export const formItemProps = {
|
|||
validateStatus: PropTypes.oneOf(tuple('', 'success', 'warning', 'error', 'validating')),
|
||||
validateTrigger: { type: [String, Array] as PropType<string | string[]> },
|
||||
messageVariables: { type: Object as PropType<Record<string, string>> },
|
||||
hidden: Boolean,
|
||||
};
|
||||
|
||||
export type FormItemProps = Partial<ExtractPropTypes<typeof formItemProps>>;
|
||||
|
||||
let indexGuid = 0;
|
||||
export default defineComponent({
|
||||
name: 'AFormItem',
|
||||
mixins: [BaseMixin],
|
||||
inheritAttrs: false,
|
||||
__ANT_NEW_FORM_ITEM: true,
|
||||
props: formItemProps,
|
||||
setup(props) {
|
||||
const FormContext = inject('FormContext', {}) as any;
|
||||
slots: ['help', 'label', 'extra'],
|
||||
setup(props, { slots }) {
|
||||
warning(props.prop === undefined, `\`prop\` is deprecated. Please use \`name\` instead.`);
|
||||
const eventKey = `form-item-${++indexGuid}`;
|
||||
const { prefixCls } = useConfigInject('form', props);
|
||||
const formContext = useInjectForm();
|
||||
const fieldName = computed(() => props.name || props.prop);
|
||||
const errors = ref([]);
|
||||
const validateDisabled = ref(false);
|
||||
const domErrorVisible = ref(false);
|
||||
const inputRef = ref();
|
||||
const namePath = computed(() => {
|
||||
const val = fieldName.value;
|
||||
return getNamePath(val);
|
||||
|
@ -119,26 +129,30 @@ export default defineComponent({
|
|||
} else if (!namePath.value.length) {
|
||||
return undefined;
|
||||
} else {
|
||||
const formName = FormContext.name;
|
||||
const formName = formContext.name.value;
|
||||
const mergedId = namePath.value.join('_');
|
||||
return formName ? `${formName}_${mergedId}` : mergedId;
|
||||
}
|
||||
});
|
||||
const fieldValue = computed(() => {
|
||||
const model = FormContext.model;
|
||||
const model = formContext.model.value;
|
||||
if (!model || !fieldName.value) {
|
||||
return;
|
||||
}
|
||||
return getPropByPath(model, namePath.value, true).v;
|
||||
});
|
||||
|
||||
const initialValue = ref(cloneDeep(fieldValue.value));
|
||||
const mergedValidateTrigger = computed(() => {
|
||||
let validateTrigger =
|
||||
props.validateTrigger !== undefined ? props.validateTrigger : FormContext.validateTrigger;
|
||||
props.validateTrigger !== undefined
|
||||
? props.validateTrigger
|
||||
: formContext.validateTrigger.value;
|
||||
validateTrigger = validateTrigger === undefined ? 'change' : validateTrigger;
|
||||
return toArray(validateTrigger);
|
||||
});
|
||||
const getRules = () => {
|
||||
let formRules = FormContext.rules;
|
||||
const rulesRef = computed<ValidationRule[]>(() => {
|
||||
let formRules = formContext.rules.value;
|
||||
const selfRules = props.rules;
|
||||
const requiredRule =
|
||||
props.required !== undefined
|
||||
|
@ -152,9 +166,9 @@ export default defineComponent({
|
|||
} else {
|
||||
return rules.concat(requiredRule);
|
||||
}
|
||||
};
|
||||
});
|
||||
const isRequired = computed(() => {
|
||||
const rules = getRules();
|
||||
const rules = rulesRef.value;
|
||||
let isRequired = false;
|
||||
if (rules && rules.length) {
|
||||
rules.every(rule => {
|
||||
|
@ -167,351 +181,233 @@ export default defineComponent({
|
|||
}
|
||||
return isRequired || props.required;
|
||||
});
|
||||
return {
|
||||
isFormItemChildren: inject('isFormItemChildren', false),
|
||||
configProvider: inject('configProvider', defaultConfigProvider),
|
||||
FormContext,
|
||||
fieldId,
|
||||
fieldName,
|
||||
namePath,
|
||||
isRequired,
|
||||
getRules,
|
||||
fieldValue,
|
||||
mergedValidateTrigger,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
warning(!hasProp(this, 'prop'), `\`prop\` is deprecated. Please use \`name\` instead.`);
|
||||
return {
|
||||
validateState: this.validateStatus,
|
||||
validateMessage: '',
|
||||
validateDisabled: false,
|
||||
validator: {},
|
||||
helpShow: false,
|
||||
errors: [],
|
||||
initialValue: undefined,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
validateStatus(val) {
|
||||
this.validateState = val;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
provide('isFormItemChildren', true);
|
||||
},
|
||||
mounted() {
|
||||
if (this.fieldName) {
|
||||
const { addField } = this.FormContext;
|
||||
addField && addField(this);
|
||||
this.initialValue = cloneDeep(this.fieldValue);
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
const { removeField } = this.FormContext;
|
||||
removeField && removeField(this);
|
||||
},
|
||||
methods: {
|
||||
getNamePath() {
|
||||
const { fieldName } = this;
|
||||
const { prefixName = [] } = this.FormContext;
|
||||
|
||||
return fieldName !== undefined ? [...prefixName, ...this.namePath] : [];
|
||||
},
|
||||
validateRules(options: ValidateOptions) {
|
||||
const { validateFirst = false, messageVariables } = this.$props;
|
||||
const validateState = ref();
|
||||
watchEffect(() => {
|
||||
validateState.value = props.validateStatus;
|
||||
});
|
||||
|
||||
const validateRules = (options: ValidateOptions) => {
|
||||
const { validateFirst = false, messageVariables } = props;
|
||||
const { triggerName } = options || {};
|
||||
const namePath = this.getNamePath();
|
||||
|
||||
let filteredRules = this.getRules();
|
||||
let filteredRules = rulesRef.value;
|
||||
if (triggerName) {
|
||||
filteredRules = filteredRules.filter(rule => {
|
||||
const { trigger } = rule;
|
||||
if (!trigger && !this.mergedValidateTrigger.length) {
|
||||
if (!trigger && !mergedValidateTrigger.value.length) {
|
||||
return true;
|
||||
}
|
||||
const triggerList = toArray(trigger || this.mergedValidateTrigger);
|
||||
const triggerList = toArray(trigger || mergedValidateTrigger.value);
|
||||
return triggerList.includes(triggerName);
|
||||
});
|
||||
}
|
||||
if (!filteredRules.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const promise = validateRules(
|
||||
namePath,
|
||||
this.fieldValue,
|
||||
filteredRules,
|
||||
const promise = validateRulesUtil(
|
||||
namePath.value,
|
||||
fieldValue.value,
|
||||
filteredRules as RuleObject[],
|
||||
options,
|
||||
validateFirst,
|
||||
messageVariables,
|
||||
);
|
||||
this.validateState = 'validating';
|
||||
this.errors = [];
|
||||
validateState.value = 'validating';
|
||||
errors.value = [];
|
||||
|
||||
promise
|
||||
.catch(e => e)
|
||||
.then((errors = []) => {
|
||||
if (this.validateState === 'validating') {
|
||||
this.validateState = errors.length ? 'error' : 'success';
|
||||
this.validateMessage = errors[0];
|
||||
this.errors = errors;
|
||||
.then((ers = []) => {
|
||||
if (validateState.value === 'validating') {
|
||||
validateState.value = ers.length ? 'error' : 'success';
|
||||
errors.value = ers;
|
||||
}
|
||||
});
|
||||
|
||||
return promise;
|
||||
},
|
||||
onFieldBlur() {
|
||||
this.validateRules({ triggerName: 'blur' });
|
||||
},
|
||||
onFieldChange() {
|
||||
if (this.validateDisabled) {
|
||||
this.validateDisabled = false;
|
||||
};
|
||||
|
||||
const onFieldBlur = () => {
|
||||
validateRules({ triggerName: 'blur' });
|
||||
};
|
||||
const onFieldChange = () => {
|
||||
if (validateDisabled.value) {
|
||||
validateDisabled.value = false;
|
||||
return;
|
||||
}
|
||||
this.validateRules({ triggerName: 'change' });
|
||||
},
|
||||
clearValidate() {
|
||||
this.validateState = '';
|
||||
this.validateMessage = '';
|
||||
this.validateDisabled = false;
|
||||
},
|
||||
resetField() {
|
||||
this.validateState = '';
|
||||
this.validateMessage = '';
|
||||
const model = this.FormContext.model || {};
|
||||
const value = this.fieldValue;
|
||||
const prop = getPropByPath(model, this.namePath, true);
|
||||
this.validateDisabled = true;
|
||||
validateRules({ triggerName: 'change' });
|
||||
};
|
||||
const clearValidate = () => {
|
||||
validateState.value = '';
|
||||
validateDisabled.value = false;
|
||||
errors.value = [];
|
||||
};
|
||||
|
||||
const resetField = () => {
|
||||
validateState.value = '';
|
||||
validateDisabled.value = true;
|
||||
errors.value = [];
|
||||
const model = formContext.model.value || {};
|
||||
const value = fieldValue.value;
|
||||
const prop = getPropByPath(model, namePath.value, true);
|
||||
if (Array.isArray(value)) {
|
||||
prop.o[prop.k] = [].concat(this.initialValue);
|
||||
prop.o[prop.k] = [].concat(initialValue.value);
|
||||
} else {
|
||||
prop.o[prop.k] = this.initialValue;
|
||||
prop.o[prop.k] = initialValue.value;
|
||||
}
|
||||
// reset validateDisabled after onFieldChange triggered
|
||||
nextTick(() => {
|
||||
this.validateDisabled = false;
|
||||
validateDisabled.value = false;
|
||||
});
|
||||
},
|
||||
getHelpMessage() {
|
||||
const help = getComponent(this, 'help');
|
||||
};
|
||||
|
||||
return this.validateMessage || help;
|
||||
},
|
||||
|
||||
onLabelClick() {
|
||||
const id = this.fieldId;
|
||||
if (!id) {
|
||||
const onLabelClick = () => {
|
||||
const id = fieldId.value;
|
||||
if (!id || !inputRef.value) {
|
||||
return;
|
||||
}
|
||||
const formItemNode = findDOMNode(this);
|
||||
const control = formItemNode.querySelector(`[id="${id}"]`);
|
||||
const control = inputRef.value.$el.querySelector(`[id="${id}"]`);
|
||||
if (control && control.focus) {
|
||||
control.focus();
|
||||
}
|
||||
},
|
||||
};
|
||||
formContext.addField(eventKey, {
|
||||
fieldValue,
|
||||
fieldId,
|
||||
fieldName,
|
||||
resetField,
|
||||
clearValidate,
|
||||
namePath,
|
||||
validateRules,
|
||||
rules: rulesRef,
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
formContext.removeField(eventKey);
|
||||
});
|
||||
// const onHelpAnimEnd = (_key: string, helpShow: boolean) => {
|
||||
// this.helpShow = helpShow;
|
||||
// if (!helpShow) {
|
||||
// this.$forceUpdate();
|
||||
// }
|
||||
// };
|
||||
const itemClassName = computed(() => ({
|
||||
[`${prefixCls.value}-item`]: true,
|
||||
|
||||
onHelpAnimEnd(_key: string, helpShow: boolean) {
|
||||
this.helpShow = helpShow;
|
||||
if (!helpShow) {
|
||||
this.$forceUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
renderHelp(prefixCls: string) {
|
||||
const help = this.getHelpMessage();
|
||||
const children = help ? (
|
||||
<div class={`${prefixCls}-explain`} key="help">
|
||||
{help}
|
||||
</div>
|
||||
) : null;
|
||||
if (children) {
|
||||
this.helpShow = !!children;
|
||||
}
|
||||
const transitionProps = getTransitionProps('show-help', {
|
||||
onAfterEnter: () => this.onHelpAnimEnd('help', true),
|
||||
onAfterLeave: () => this.onHelpAnimEnd('help', false),
|
||||
});
|
||||
return (
|
||||
<Transition {...transitionProps} key="help">
|
||||
{children}
|
||||
</Transition>
|
||||
);
|
||||
},
|
||||
|
||||
renderExtra(prefixCls: string) {
|
||||
const extra = getComponent(this, 'extra');
|
||||
return extra ? <div class={`${prefixCls}-extra`}>{extra}</div> : null;
|
||||
},
|
||||
|
||||
renderValidateWrapper(prefixCls: string, c1: VueNode, c2: VueNode, c3: VueNode) {
|
||||
const validateStatus = this.validateState;
|
||||
|
||||
let classes = `${prefixCls}-item-control`;
|
||||
if (validateStatus) {
|
||||
classes = classNames(`${prefixCls}-item-control`, {
|
||||
'has-feedback': validateStatus && this.hasFeedback,
|
||||
'has-success': validateStatus === 'success',
|
||||
'has-warning': validateStatus === 'warning',
|
||||
'has-error': validateStatus === 'error',
|
||||
'is-validating': validateStatus === 'validating',
|
||||
// Status
|
||||
[`${prefixCls.value}-item-has-feedback`]: validateState.value && props.hasFeedback,
|
||||
[`${prefixCls.value}-item-has-success`]: validateState.value === 'success',
|
||||
[`${prefixCls.value}-item-has-warning`]: validateState.value === 'warning',
|
||||
[`${prefixCls.value}-item-has-error`]: validateState.value === 'error',
|
||||
[`${prefixCls.value}-item-is-validating`]: validateState.value === 'validating',
|
||||
[`${prefixCls.value}-item-hidden`]: props.hidden,
|
||||
}));
|
||||
return () => {
|
||||
const help = props.help ?? (slots.help ? filterEmpty(slots.help()) : null);
|
||||
const children = flattenChildren(slots.default?.());
|
||||
let firstChildren = children[0];
|
||||
if (fieldName.value && props.autoLink && isValidElement(firstChildren)) {
|
||||
const originalEvents = firstChildren.props;
|
||||
const originalBlur = originalEvents.onBlur;
|
||||
const originalChange = originalEvents.onChange;
|
||||
firstChildren = cloneElement(firstChildren, {
|
||||
...(fieldId.value ? { id: fieldId.value } : undefined),
|
||||
onBlur: (...args: any[]) => {
|
||||
if (Array.isArray(originalChange)) {
|
||||
for (let i = 0, l = originalChange.length; i < l; i++) {
|
||||
originalBlur[i](...args);
|
||||
}
|
||||
} else if (originalBlur) {
|
||||
originalBlur(...args);
|
||||
}
|
||||
onFieldBlur();
|
||||
},
|
||||
onChange: (...args: any[]) => {
|
||||
if (Array.isArray(originalChange)) {
|
||||
for (let i = 0, l = originalChange.length; i < l; i++) {
|
||||
originalChange[i](...args);
|
||||
}
|
||||
} else if (originalChange) {
|
||||
originalChange(...args);
|
||||
}
|
||||
onFieldChange();
|
||||
},
|
||||
});
|
||||
}
|
||||
const IconNode = validateStatus && iconMap[validateStatus];
|
||||
|
||||
const icon =
|
||||
this.hasFeedback && IconNode ? (
|
||||
<span class={`${prefixCls}-item-children-icon`}>
|
||||
<IconNode />
|
||||
</span>
|
||||
) : null;
|
||||
return (
|
||||
<div class={classes}>
|
||||
<span class={`${prefixCls}-item-children`}>
|
||||
{c1}
|
||||
{icon}
|
||||
</span>
|
||||
{c2}
|
||||
{c3}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
renderWrapper(prefixCls: string, children: VueNode) {
|
||||
const { wrapperCol: contextWrapperCol } = (this.isFormItemChildren
|
||||
? {}
|
||||
: this.FormContext) as any;
|
||||
const { wrapperCol } = this;
|
||||
const mergedWrapperCol = wrapperCol || contextWrapperCol || {};
|
||||
const { style, id, ...restProps } = mergedWrapperCol;
|
||||
const className = classNames(`${prefixCls}-item-control-wrapper`, mergedWrapperCol.class);
|
||||
const colProps = {
|
||||
...restProps,
|
||||
class: className,
|
||||
key: 'wrapper',
|
||||
style,
|
||||
id,
|
||||
};
|
||||
return <Col {...colProps}>{children}</Col>;
|
||||
},
|
||||
|
||||
renderLabel(prefixCls: string) {
|
||||
const {
|
||||
vertical,
|
||||
labelAlign: contextLabelAlign,
|
||||
labelCol: contextLabelCol,
|
||||
colon: contextColon,
|
||||
} = this.FormContext;
|
||||
const { labelAlign, labelCol, colon, fieldId, htmlFor } = this;
|
||||
const label = getComponent(this, 'label');
|
||||
const required = this.isRequired;
|
||||
const mergedLabelCol = labelCol || contextLabelCol || {};
|
||||
|
||||
const mergedLabelAlign = labelAlign || contextLabelAlign;
|
||||
const labelClsBasic = `${prefixCls}-item-label`;
|
||||
const labelColClassName = classNames(
|
||||
labelClsBasic,
|
||||
mergedLabelAlign === 'left' && `${labelClsBasic}-left`,
|
||||
mergedLabelCol.class,
|
||||
);
|
||||
const {
|
||||
class: labelColClass,
|
||||
style: labelColStyle,
|
||||
id: labelColId,
|
||||
...restProps
|
||||
} = mergedLabelCol;
|
||||
let labelChildren = label;
|
||||
// Keep label is original where there should have no colon
|
||||
const computedColon = colon === true || (contextColon !== false && colon !== false);
|
||||
const haveColon = computedColon && !vertical;
|
||||
// Remove duplicated user input colon
|
||||
if (haveColon && typeof label === 'string' && label.trim() !== '') {
|
||||
labelChildren = label.replace(/[::]\s*$/, '');
|
||||
}
|
||||
|
||||
const labelClassName = classNames({
|
||||
[`${prefixCls}-item-required`]: required,
|
||||
[`${prefixCls}-item-no-colon`]: !computedColon,
|
||||
});
|
||||
const colProps = {
|
||||
...restProps,
|
||||
class: labelColClassName,
|
||||
key: 'label',
|
||||
style: labelColStyle,
|
||||
id: labelColId,
|
||||
};
|
||||
|
||||
return label ? (
|
||||
<Col {...colProps}>
|
||||
<label
|
||||
for={htmlFor || fieldId}
|
||||
class={labelClassName}
|
||||
title={typeof label === 'string' ? label : ''}
|
||||
onClick={this.onLabelClick}
|
||||
<Row
|
||||
class={[
|
||||
itemClassName.value,
|
||||
domErrorVisible.value || !!help ? `${prefixCls.value}-item-with-help` : '',
|
||||
]}
|
||||
key="row"
|
||||
>
|
||||
{/* Label */}
|
||||
<FormItemLabel
|
||||
{...props}
|
||||
htmlFor={fieldId.value}
|
||||
required={isRequired.value}
|
||||
requiredMark={formContext.requiredMark.value}
|
||||
prefixCls={prefixCls.value}
|
||||
onClick={onLabelClick}
|
||||
label={props.label ?? slots.label?.()}
|
||||
/>
|
||||
{/* Input Group */}
|
||||
<FormItemInput
|
||||
{...props}
|
||||
errors={help !== undefined && help !== null ? toArray(help) : errors.value}
|
||||
prefixCls={prefixCls.value}
|
||||
status={validateState.value}
|
||||
onDomErrorVisibleChange={(v: boolean) => (domErrorVisible.value = v)}
|
||||
validateStatus={validateState.value}
|
||||
ref={inputRef}
|
||||
help={help}
|
||||
extra={props.extra ?? slots.extra?.()}
|
||||
>
|
||||
{labelChildren}
|
||||
</label>
|
||||
</Col>
|
||||
) : null;
|
||||
},
|
||||
renderChildren(prefixCls: string, child: VueNode) {
|
||||
return [
|
||||
this.renderLabel(prefixCls),
|
||||
this.renderWrapper(
|
||||
prefixCls,
|
||||
this.renderValidateWrapper(
|
||||
prefixCls,
|
||||
child,
|
||||
this.renderHelp(prefixCls),
|
||||
this.renderExtra(prefixCls),
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
renderFormItem(child: any[]) {
|
||||
const { prefixCls: customizePrefixCls } = this.$props;
|
||||
const { class: className, ...restProps } = this.$attrs as any;
|
||||
const getPrefixCls = this.configProvider.getPrefixCls;
|
||||
const prefixCls = getPrefixCls('form', customizePrefixCls);
|
||||
const children = this.renderChildren(prefixCls, child);
|
||||
const itemClassName = {
|
||||
[className]: className,
|
||||
[`${prefixCls}-item`]: true,
|
||||
[`${prefixCls}-item-with-help`]: this.helpShow,
|
||||
};
|
||||
|
||||
return (
|
||||
<Row class={classNames(itemClassName)} key="row" {...restProps}>
|
||||
{children}
|
||||
{[firstChildren, children.slice(1)]}
|
||||
</FormItemInput>
|
||||
</Row>
|
||||
);
|
||||
},
|
||||
},
|
||||
render() {
|
||||
const { autoLink } = getOptionProps(this);
|
||||
const children = getSlot(this);
|
||||
let firstChildren = children[0];
|
||||
if (this.fieldName && autoLink && isValidElement(firstChildren)) {
|
||||
const originalEvents = getEvents(firstChildren);
|
||||
const originalBlur = originalEvents.onBlur;
|
||||
const originalChange = originalEvents.onChange;
|
||||
firstChildren = cloneElement(firstChildren, {
|
||||
...(this.fieldId ? { id: this.fieldId } : undefined),
|
||||
onBlur: (...args: any[]) => {
|
||||
originalBlur && originalBlur(...args);
|
||||
this.onFieldBlur();
|
||||
},
|
||||
onChange: (...args: any[]) => {
|
||||
if (Array.isArray(originalChange)) {
|
||||
for (let i = 0, l = originalChange.length; i < l; i++) {
|
||||
originalChange[i](...args);
|
||||
}
|
||||
} else if (originalChange) {
|
||||
originalChange(...args);
|
||||
}
|
||||
this.onFieldChange();
|
||||
},
|
||||
});
|
||||
}
|
||||
return this.renderFormItem([firstChildren, children.slice(1)]);
|
||||
};
|
||||
},
|
||||
// data() {
|
||||
// warning(!hasProp(this, 'prop'), `\`prop\` is deprecated. Please use \`name\` instead.`);
|
||||
// return {
|
||||
// validateState: this.validateStatus,
|
||||
// validateMessage: '',
|
||||
// validateDisabled: false,
|
||||
// validator: {},
|
||||
// helpShow: false,
|
||||
// errors: [],
|
||||
// initialValue: undefined,
|
||||
// };
|
||||
// },
|
||||
// render() {
|
||||
// const { autoLink } = getOptionProps(this);
|
||||
// const children = getSlot(this);
|
||||
// let firstChildren = children[0];
|
||||
// if (this.fieldName && autoLink && isValidElement(firstChildren)) {
|
||||
// const originalEvents = getEvents(firstChildren);
|
||||
// const originalBlur = originalEvents.onBlur;
|
||||
// const originalChange = originalEvents.onChange;
|
||||
// firstChildren = cloneElement(firstChildren, {
|
||||
// ...(this.fieldId ? { id: this.fieldId } : undefined),
|
||||
// onBlur: (...args: any[]) => {
|
||||
// originalBlur && originalBlur(...args);
|
||||
// this.onFieldBlur();
|
||||
// },
|
||||
// onChange: (...args: any[]) => {
|
||||
// if (Array.isArray(originalChange)) {
|
||||
// for (let i = 0, l = originalChange.length; i < l; i++) {
|
||||
// originalChange[i](...args);
|
||||
// }
|
||||
// } else if (originalChange) {
|
||||
// originalChange(...args);
|
||||
// }
|
||||
// this.onFieldChange();
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// return this.renderFormItem([firstChildren, children.slice(1)]);
|
||||
// },
|
||||
});
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined';
|
||||
import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled';
|
||||
import CheckCircleFilled from '@ant-design/icons-vue/CheckCircleFilled';
|
||||
import ExclamationCircleFilled from '@ant-design/icons-vue/ExclamationCircleFilled';
|
||||
|
||||
import Col, { ColProps } from '../grid/Col';
|
||||
import { useProvideForm, useInjectForm, useProvideFormItemPrefix } from './context';
|
||||
import ErrorList from './ErrorList';
|
||||
import classNames from '../_util/classNames';
|
||||
import { ValidateStatus } from './FormItem';
|
||||
import { VueNode } from '../_util/type';
|
||||
import { computed, defineComponent, HTMLAttributes, onUnmounted } from 'vue';
|
||||
|
||||
export interface FormItemInputMiscProps {
|
||||
prefixCls: string;
|
||||
errors: VueNode[];
|
||||
hasFeedback?: boolean;
|
||||
validateStatus?: ValidateStatus;
|
||||
onDomErrorVisibleChange: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
export interface FormItemInputProps {
|
||||
wrapperCol?: ColProps;
|
||||
help?: VueNode;
|
||||
extra?: VueNode;
|
||||
status?: ValidateStatus;
|
||||
}
|
||||
|
||||
const iconMap: { [key: string]: any } = {
|
||||
success: CheckCircleFilled,
|
||||
warning: ExclamationCircleFilled,
|
||||
error: CloseCircleFilled,
|
||||
validating: LoadingOutlined,
|
||||
};
|
||||
const FormItemInput = defineComponent({
|
||||
slots: ['help', 'extra', 'errors'],
|
||||
inheritAttrs: false,
|
||||
props: [
|
||||
'prefixCls',
|
||||
'errors',
|
||||
'hasFeedback',
|
||||
'validateStatus',
|
||||
'onDomErrorVisibleChange',
|
||||
'wrapperCol',
|
||||
'help',
|
||||
'extra',
|
||||
'status',
|
||||
],
|
||||
setup(props, { slots }) {
|
||||
const formContext = useInjectForm();
|
||||
const { wrapperCol: contextWrapperCol } = formContext;
|
||||
|
||||
// Pass to sub FormItem should not with col info
|
||||
const subFormContext = { ...formContext };
|
||||
delete subFormContext.labelCol;
|
||||
delete subFormContext.wrapperCol;
|
||||
useProvideForm(subFormContext);
|
||||
useProvideFormItemPrefix({
|
||||
prefixCls: computed(() => props.prefixCls),
|
||||
status: computed(() => props.status),
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
props.onDomErrorVisibleChange(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
const {
|
||||
prefixCls,
|
||||
wrapperCol,
|
||||
help = slots.help?.(),
|
||||
errors = slots.errors?.(),
|
||||
onDomErrorVisibleChange,
|
||||
hasFeedback,
|
||||
validateStatus,
|
||||
extra = slots.extra?.(),
|
||||
} = props;
|
||||
const baseClassName = `${prefixCls}-item`;
|
||||
|
||||
const mergedWrapperCol: ColProps & HTMLAttributes =
|
||||
wrapperCol || contextWrapperCol?.value || {};
|
||||
|
||||
const className = classNames(`${baseClassName}-control`, mergedWrapperCol.class);
|
||||
|
||||
// Should provides additional icon if `hasFeedback`
|
||||
const IconNode = validateStatus && iconMap[validateStatus];
|
||||
const icon =
|
||||
hasFeedback && IconNode ? (
|
||||
<span class={`${baseClassName}-children-icon`}>
|
||||
<IconNode />
|
||||
</span>
|
||||
) : null;
|
||||
|
||||
const inputDom = (
|
||||
<div class={`${baseClassName}-control-input`}>
|
||||
<div class={`${baseClassName}-control-input-content`}>{slots.default?.()}</div>
|
||||
{icon}
|
||||
</div>
|
||||
);
|
||||
const errorListDom = (
|
||||
<ErrorList errors={errors} help={help} onDomErrorVisibleChange={onDomErrorVisibleChange} />
|
||||
);
|
||||
|
||||
// If extra = 0, && will goes wrong
|
||||
// 0&&error -> 0
|
||||
const extraDom = extra ? <div class={`${baseClassName}-extra`}>{extra}</div> : null;
|
||||
|
||||
return (
|
||||
<Col {...mergedWrapperCol} class={className}>
|
||||
{inputDom}
|
||||
{errorListDom}
|
||||
{extraDom}
|
||||
</Col>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default FormItemInput;
|
|
@ -0,0 +1,97 @@
|
|||
import Col, { ColProps } from '../grid/Col';
|
||||
import { FormLabelAlign } from './interface';
|
||||
import { useInjectForm } from './context';
|
||||
import { RequiredMark } from './Form';
|
||||
import { useLocaleReceiver } from '../locale-provider/LocaleReceiver';
|
||||
import defaultLocale from '../locale/default';
|
||||
import classNames from '../_util/classNames';
|
||||
import { VueNode } from '../_util/type';
|
||||
import { FunctionalComponent, HTMLAttributes } from 'vue';
|
||||
|
||||
export interface FormItemLabelProps {
|
||||
colon?: boolean;
|
||||
htmlFor?: string;
|
||||
label?: VueNode;
|
||||
labelAlign?: FormLabelAlign;
|
||||
labelCol?: ColProps & HTMLAttributes;
|
||||
requiredMark?: RequiredMark;
|
||||
required?: boolean;
|
||||
prefixCls: string;
|
||||
onClick: Function;
|
||||
}
|
||||
|
||||
const FormItemLabel: FunctionalComponent<FormItemLabelProps> = (props, { slots, emit, attrs }) => {
|
||||
const { prefixCls, htmlFor, labelCol, labelAlign, colon, required, requiredMark } = {
|
||||
...props,
|
||||
...attrs,
|
||||
};
|
||||
const [formLocale] = useLocaleReceiver('Form');
|
||||
const label = props.label ?? slots.label?.();
|
||||
if (!label) return null;
|
||||
const {
|
||||
vertical,
|
||||
labelAlign: contextLabelAlign,
|
||||
labelCol: contextLabelCol,
|
||||
colon: contextColon,
|
||||
} = useInjectForm();
|
||||
const mergedLabelCol: FormItemLabelProps['labelCol'] = labelCol || contextLabelCol?.value || {};
|
||||
|
||||
const mergedLabelAlign: FormLabelAlign | undefined = labelAlign || contextLabelAlign?.value;
|
||||
|
||||
const labelClsBasic = `${prefixCls}-item-label`;
|
||||
const labelColClassName = classNames(
|
||||
labelClsBasic,
|
||||
mergedLabelAlign === 'left' && `${labelClsBasic}-left`,
|
||||
mergedLabelCol.class,
|
||||
);
|
||||
|
||||
let labelChildren = label;
|
||||
// Keep label is original where there should have no colon
|
||||
const computedColon = colon === true || (contextColon?.value !== false && colon !== false);
|
||||
const haveColon = computedColon && !vertical.value;
|
||||
// Remove duplicated user input colon
|
||||
if (haveColon && typeof label === 'string' && (label as string).trim() !== '') {
|
||||
labelChildren = (label as string).replace(/[:|:]\s*$/, '');
|
||||
}
|
||||
|
||||
labelChildren = (
|
||||
<>
|
||||
{labelChildren}
|
||||
{slots.tooltip?.({ class: `${prefixCls}-item-tooltip` })}
|
||||
</>
|
||||
);
|
||||
|
||||
// Add required mark if optional
|
||||
if (requiredMark === 'optional' && !required) {
|
||||
labelChildren = (
|
||||
<>
|
||||
{labelChildren}
|
||||
<span class={`${prefixCls}-item-optional`}>
|
||||
{formLocale.value?.optional || defaultLocale.Form?.optional}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
const labelClassName = classNames({
|
||||
[`${prefixCls}-item-required`]: required,
|
||||
[`${prefixCls}-item-required-mark-optional`]: requiredMark === 'optional',
|
||||
[`${prefixCls}-item-no-colon`]: !computedColon,
|
||||
});
|
||||
return (
|
||||
<Col {...mergedLabelCol} class={labelColClassName}>
|
||||
<label
|
||||
html-for={htmlFor}
|
||||
class={labelClassName}
|
||||
title={typeof label === 'string' ? label : ''}
|
||||
onClick={e => emit('click', e)}
|
||||
>
|
||||
{labelChildren}
|
||||
</label>
|
||||
</Col>
|
||||
);
|
||||
};
|
||||
|
||||
FormItemLabel.displayName = 'FormItemLabel';
|
||||
FormItemLabel.inheritAttrs = false;
|
||||
|
||||
export default FormItemLabel;
|
|
@ -0,0 +1,58 @@
|
|||
import { inject, InjectionKey, provide, ComputedRef, computed } from 'vue';
|
||||
import { ColProps } from '../grid';
|
||||
import { RequiredMark, ValidationRule } from './Form';
|
||||
import { ValidateStatus, FieldExpose } from './FormItem';
|
||||
import { FormLabelAlign } from './interface';
|
||||
|
||||
export interface FormContextProps {
|
||||
model?: ComputedRef<any>;
|
||||
vertical: ComputedRef<boolean>;
|
||||
name?: ComputedRef<string>;
|
||||
colon?: ComputedRef<boolean>;
|
||||
labelAlign?: ComputedRef<FormLabelAlign>;
|
||||
labelCol?: ComputedRef<ColProps>;
|
||||
wrapperCol?: ComputedRef<ColProps>;
|
||||
requiredMark?: ComputedRef<RequiredMark>;
|
||||
//itemRef: (name: (string | number)[]) => (node: React.ReactElement) => void;
|
||||
addField: (eventKey: string, field: FieldExpose) => void;
|
||||
removeField: (eventKey: string) => void;
|
||||
validateTrigger?: ComputedRef<string | string[]>;
|
||||
rules?: ComputedRef<{ [k: string]: ValidationRule[] | ValidationRule }>;
|
||||
}
|
||||
|
||||
export const FormContextKey: InjectionKey<FormContextProps> = Symbol('formContextKey');
|
||||
|
||||
export const useProvideForm = (state: FormContextProps) => {
|
||||
provide(FormContextKey, state);
|
||||
};
|
||||
|
||||
export const useInjectForm = () => {
|
||||
return inject(FormContextKey, {
|
||||
labelAlign: computed(() => 'right' as FormLabelAlign),
|
||||
vertical: computed(() => false),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
addField: (_eventKey: string, _field: FieldExpose) => {},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
removeField: (_eventKey: string) => {},
|
||||
});
|
||||
};
|
||||
|
||||
/** Used for ErrorList only */
|
||||
export interface FormItemPrefixContextProps {
|
||||
prefixCls: ComputedRef<string>;
|
||||
status?: ComputedRef<ValidateStatus>;
|
||||
}
|
||||
|
||||
export const FormItemPrefixContextKey: InjectionKey<FormItemPrefixContextProps> = Symbol(
|
||||
'formItemPrefixContextKey',
|
||||
);
|
||||
|
||||
export const useProvideFormItemPrefix = (state: FormItemPrefixContextProps) => {
|
||||
provide(FormItemPrefixContextKey, state);
|
||||
};
|
||||
|
||||
export const useInjectFormItemPrefix = () => {
|
||||
return inject(FormItemPrefixContextKey, {
|
||||
prefixCls: computed(() => ''),
|
||||
});
|
||||
};
|
|
@ -1,5 +1,7 @@
|
|||
import { VueNode } from '../_util/type';
|
||||
|
||||
export type FormLabelAlign = 'left' | 'right';
|
||||
|
||||
export type InternalNamePath = (string | number)[];
|
||||
export type NamePath = string | number | InternalNamePath;
|
||||
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
@import './index';
|
||||
|
||||
// ================================================================
|
||||
// = Children Component =
|
||||
// ================================================================
|
||||
.@{form-item-prefix-cls} {
|
||||
.@{ant-prefix}-mentions,
|
||||
textarea.@{ant-prefix}-input {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
// input[type=file]
|
||||
.@{ant-prefix}-upload {
|
||||
background: transparent;
|
||||
}
|
||||
.@{ant-prefix}-upload.@{ant-prefix}-upload-drag {
|
||||
background: @background-color-light;
|
||||
}
|
||||
|
||||
input[type='radio'],
|
||||
input[type='checkbox'] {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
// Radios and checkboxes on same line
|
||||
.@{ant-prefix}-radio-inline,
|
||||
.@{ant-prefix}-checkbox-inline {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
font-weight: normal;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.@{ant-prefix}-checkbox-vertical,
|
||||
.@{ant-prefix}-radio-vertical {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.@{ant-prefix}-checkbox-vertical + .@{ant-prefix}-checkbox-vertical,
|
||||
.@{ant-prefix}-radio-vertical + .@{ant-prefix}-radio-vertical {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.@{ant-prefix}-input-number {
|
||||
+ .@{form-prefix-cls}-text {
|
||||
margin-left: 8px;
|
||||
}
|
||||
&-handler-wrap {
|
||||
z-index: 2; // https://github.com/ant-design/ant-design/issues/6289
|
||||
}
|
||||
}
|
||||
|
||||
.@{ant-prefix}-select,
|
||||
.@{ant-prefix}-cascader-picker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Don't impact select inside input group and calendar header select
|
||||
.@{ant-prefix}-picker-calendar-year-select,
|
||||
.@{ant-prefix}-picker-calendar-month-select,
|
||||
.@{ant-prefix}-input-group .@{ant-prefix}-select,
|
||||
.@{ant-prefix}-input-group .@{ant-prefix}-cascader-picker {
|
||||
width: auto;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
@import './index';
|
||||
|
||||
.@{form-prefix-cls}-horizontal {
|
||||
.@{form-item-prefix-cls}-label {
|
||||
flex-grow: 0;
|
||||
}
|
||||
.@{form-item-prefix-cls}-control {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
}
|
|
@ -3,94 +3,82 @@
|
|||
@import '../../input/style/mixin';
|
||||
@import '../../button/style/mixin';
|
||||
@import '../../grid/style/mixin';
|
||||
@import './components';
|
||||
@import './inline';
|
||||
@import './horizontal';
|
||||
@import './vertical';
|
||||
@import './status';
|
||||
@import './mixin';
|
||||
|
||||
@form-prefix-cls: ~'@{ant-prefix}-form';
|
||||
@form-component-height: @input-height-base;
|
||||
@form-component-max-height: @input-height-lg;
|
||||
@form-feedback-icon-size: @font-size-base;
|
||||
@form-help-margin-top: ((@form-component-height - @form-component-max-height) / 2) + 2px;
|
||||
@form-explain-font-size: @font-size-base;
|
||||
// Extends additional 1px to fix precision issue.
|
||||
// https://github.com/ant-design/ant-design/issues/12803
|
||||
// https://github.com/ant-design/ant-design/issues/8220
|
||||
@form-explain-precision: 1px;
|
||||
@form-explain-height: floor(@form-explain-font-size * @line-height-base);
|
||||
@form-item-prefix-cls: ~'@{form-prefix-cls}-item';
|
||||
@form-font-height: ceil(@font-size-base * @line-height-base);
|
||||
|
||||
.@{form-prefix-cls} {
|
||||
.reset-component();
|
||||
.reset-form();
|
||||
}
|
||||
|
||||
.@{form-prefix-cls}-item-required::before {
|
||||
display: inline-block;
|
||||
margin-right: 4px;
|
||||
color: @label-required-color;
|
||||
font-size: @font-size-base;
|
||||
font-family: SimSun, sans-serif;
|
||||
line-height: 1;
|
||||
content: '*';
|
||||
.@{form-prefix-cls}-hide-required-mark & {
|
||||
display: none;
|
||||
.@{form-prefix-cls}-text {
|
||||
display: inline-block;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.@{form-prefix-cls}-item-label > label {
|
||||
color: @label-color;
|
||||
|
||||
&::after {
|
||||
& when (@form-item-trailing-colon=true) {
|
||||
content: ':';
|
||||
}
|
||||
& when not (@form-item-trailing-colon=true) {
|
||||
content: ' ';
|
||||
// ================================================================
|
||||
// = Size =
|
||||
// ================================================================
|
||||
.formSize(@input-height) {
|
||||
.@{form-item-prefix-cls}-label > label {
|
||||
height: @input-height;
|
||||
}
|
||||
|
||||
position: relative;
|
||||
top: -0.5px;
|
||||
margin: 0 @form-item-label-colon-margin-right 0 @form-item-label-colon-margin-left;
|
||||
}
|
||||
|
||||
&.@{form-prefix-cls}-item-no-colon::after {
|
||||
content: ' ';
|
||||
}
|
||||
}
|
||||
|
||||
// Form items
|
||||
// You should wrap labels and controls in .@{form-prefix-cls}-item for optimum spacing
|
||||
.@{form-prefix-cls}-item {
|
||||
label {
|
||||
position: relative;
|
||||
|
||||
> .@{iconfont-css-prefix} {
|
||||
font-size: @font-size-base;
|
||||
vertical-align: top;
|
||||
.@{form-item-prefix-cls}-control-input {
|
||||
min-height: @input-height;
|
||||
}
|
||||
}
|
||||
|
||||
&-small {
|
||||
.formSize(@input-height-sm);
|
||||
}
|
||||
&-large {
|
||||
.formSize(@input-height-lg);
|
||||
}
|
||||
}
|
||||
|
||||
.explainAndExtraDistance(@num) when (@num >= 0) {
|
||||
padding-top: floor(@num);
|
||||
}
|
||||
|
||||
.explainAndExtraDistance(@num) when (@num < 0) {
|
||||
margin-top: ceil(@num);
|
||||
margin-bottom: ceil(@num);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// = Item =
|
||||
// ================================================================
|
||||
.@{form-item-prefix-cls} {
|
||||
.reset-component();
|
||||
|
||||
margin-bottom: @form-item-margin-bottom;
|
||||
vertical-align: top;
|
||||
|
||||
&-control {
|
||||
position: relative;
|
||||
line-height: @form-component-max-height;
|
||||
.clearfix();
|
||||
}
|
||||
|
||||
&-children {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&-with-help {
|
||||
margin-bottom: max(0, @form-item-margin-bottom - @form-explain-height - @form-help-margin-top);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&-hidden,
|
||||
&-hidden.@{ant-prefix}-row {
|
||||
// https://github.com/ant-design/ant-design/issues/26141
|
||||
display: none;
|
||||
}
|
||||
|
||||
// ==============================================================
|
||||
// = Label =
|
||||
// ==============================================================
|
||||
&-label {
|
||||
display: inline-block;
|
||||
flex-grow: 0;
|
||||
overflow: hidden;
|
||||
line-height: @form-component-max-height - 0.0001px;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
vertical-align: middle;
|
||||
|
@ -98,505 +86,126 @@
|
|||
&-left {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.@{ant-prefix}-switch {
|
||||
margin: 2px 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.@{form-prefix-cls}-explain,
|
||||
.@{form-prefix-cls}-extra {
|
||||
clear: both;
|
||||
min-height: @form-explain-height + @form-explain-precision;
|
||||
margin-top: @form-help-margin-top;
|
||||
color: @text-color-secondary;
|
||||
font-size: @form-explain-font-size;
|
||||
line-height: @line-height-base;
|
||||
transition: color 0.3s @ease-out; // sync input color transition
|
||||
}
|
||||
|
||||
.@{form-prefix-cls}-explain {
|
||||
margin-bottom: -@form-explain-precision;
|
||||
}
|
||||
|
||||
.@{form-prefix-cls}-extra {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.@{form-prefix-cls}-text {
|
||||
display: inline-block;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.@{form-prefix-cls}-split {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
form {
|
||||
.has-feedback {
|
||||
// https://github.com/ant-design/ant-design/issues/19884
|
||||
.@{ant-prefix}-input-affix-wrapper {
|
||||
.@{ant-prefix}-input-suffix {
|
||||
padding-right: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
// Fix overlapping between feedback icon and <Select>'s arrow.
|
||||
// https://github.com/ant-design/ant-design/issues/4431
|
||||
> .@{ant-prefix}-select .@{ant-prefix}-select-arrow,
|
||||
> .@{ant-prefix}-select .@{ant-prefix}-select-selection__clear,
|
||||
:not(.@{ant-prefix}-input-group-addon) > .@{ant-prefix}-select .@{ant-prefix}-select-arrow,
|
||||
:not(.@{ant-prefix}-input-group-addon)
|
||||
> .@{ant-prefix}-select
|
||||
.@{ant-prefix}-select-selection__clear {
|
||||
right: (@form-component-height / 2) + @form-feedback-icon-size - 2px;
|
||||
}
|
||||
> .@{ant-prefix}-select .@{ant-prefix}-select-selection-selected-value,
|
||||
:not(.@{ant-prefix}-input-group-addon)
|
||||
> .@{ant-prefix}-select
|
||||
.@{ant-prefix}-select-selection-selected-value {
|
||||
padding-right: 42px;
|
||||
}
|
||||
|
||||
.@{ant-prefix}-cascader-picker {
|
||||
&-arrow {
|
||||
margin-right: (@form-component-height / 2) + @form-feedback-icon-size - 13px;
|
||||
}
|
||||
&-clear {
|
||||
right: (@form-component-height / 2) + @form-feedback-icon-size - 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// Fix issue: https://github.com/ant-design/ant-design/issues/7854
|
||||
.@{ant-prefix}-input-search:not(.@{ant-prefix}-input-search-enter-button) {
|
||||
.@{ant-prefix}-input-suffix {
|
||||
right: (@form-component-height / 2) + @form-feedback-icon-size - 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// Fix issue: https://github.com/ant-design/ant-design/issues/4783
|
||||
.@{ant-prefix}-calendar-picker,
|
||||
.@{ant-prefix}-time-picker {
|
||||
&-icon,
|
||||
&-clear {
|
||||
right: (@form-component-height / 2) + @form-feedback-icon-size - 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{ant-prefix}-mentions,
|
||||
textarea.@{ant-prefix}-input {
|
||||
height: auto;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
// input[type=file]
|
||||
.@{ant-prefix}-upload {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
input[type='radio'],
|
||||
input[type='checkbox'] {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
// Radios and checkboxes on same line
|
||||
.@{ant-prefix}-radio-inline,
|
||||
.@{ant-prefix}-checkbox-inline {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
font-weight: normal;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.@{ant-prefix}-checkbox-vertical,
|
||||
.@{ant-prefix}-radio-vertical {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.@{ant-prefix}-checkbox-vertical + .@{ant-prefix}-checkbox-vertical,
|
||||
.@{ant-prefix}-radio-vertical + .@{ant-prefix}-radio-vertical {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.@{ant-prefix}-input-number {
|
||||
+ .@{form-prefix-cls}-text {
|
||||
margin-left: 8px;
|
||||
}
|
||||
&-handler-wrap {
|
||||
z-index: 2; // https://github.com/ant-design/ant-design/issues/6289
|
||||
}
|
||||
}
|
||||
|
||||
.@{ant-prefix}-select,
|
||||
.@{ant-prefix}-cascader-picker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Don't impact select inside input group
|
||||
.@{ant-prefix}-input-group .@{ant-prefix}-select,
|
||||
.@{ant-prefix}-input-group .@{ant-prefix}-cascader-picker {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
// fix input with addon position. https://github.com/ant-design/ant-design/issues/8243
|
||||
:not(.@{ant-prefix}-input-group-wrapper) > .@{ant-prefix}-input-group,
|
||||
.@{ant-prefix}-input-group-wrapper {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/20616
|
||||
&:not(.@{form-prefix-cls}-vertical) {
|
||||
:not(.@{ant-prefix}-input-group-wrapper) > .@{ant-prefix}-input-group,
|
||||
.@{ant-prefix}-input-group-wrapper {
|
||||
> label {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
// display: inline;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: @form-item-label-height;
|
||||
color: @label-color;
|
||||
font-size: @form-item-label-font-size;
|
||||
|
||||
> .@{iconfont-css-prefix} {
|
||||
font-size: @form-item-label-font-size;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
// Required mark
|
||||
&.@{form-item-prefix-cls}-required:not(.@{form-item-prefix-cls}-required-mark-optional)::before {
|
||||
display: inline-block;
|
||||
margin-right: 4px;
|
||||
color: @label-required-color;
|
||||
font-size: @form-item-label-font-size;
|
||||
font-family: SimSun, sans-serif;
|
||||
line-height: 1;
|
||||
content: '*';
|
||||
|
||||
.@{form-prefix-cls}-hide-required-mark & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Optional mark
|
||||
.@{form-item-prefix-cls}-optional {
|
||||
display: inline-block;
|
||||
margin-left: @margin-xss;
|
||||
color: @text-color-secondary;
|
||||
|
||||
.@{form-prefix-cls}-hide-required-mark & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Optional mark
|
||||
.@{form-item-prefix-cls}-tooltip {
|
||||
color: @text-color-secondary;
|
||||
cursor: help;
|
||||
writing-mode: horizontal-tb;
|
||||
margin-inline-start: @margin-xss;
|
||||
}
|
||||
|
||||
&::after {
|
||||
& when (@form-item-trailing-colon=true) {
|
||||
content: ':';
|
||||
}
|
||||
& when not (@form-item-trailing-colon=true) {
|
||||
content: ' ';
|
||||
}
|
||||
|
||||
position: relative;
|
||||
top: -0.5px;
|
||||
margin: 0 @form-item-label-colon-margin-right 0 @form-item-label-colon-margin-left;
|
||||
}
|
||||
|
||||
&.@{form-item-prefix-cls}-no-colon::after {
|
||||
content: ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Form layout
|
||||
//== Vertical Form
|
||||
.make-vertical-layout-label() {
|
||||
display: block;
|
||||
margin: @form-vertical-label-margin;
|
||||
padding: @form-vertical-label-padding;
|
||||
line-height: @line-height-base;
|
||||
white-space: initial;
|
||||
text-align: left;
|
||||
|
||||
label::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.make-vertical-layout() {
|
||||
.@{form-prefix-cls}-item-label,
|
||||
.@{form-prefix-cls}-item-control-wrapper {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.@{form-prefix-cls}-item-label {
|
||||
.make-vertical-layout-label();
|
||||
}
|
||||
}
|
||||
|
||||
.@{form-prefix-cls}-vertical {
|
||||
.@{form-prefix-cls}-item {
|
||||
// ==============================================================
|
||||
// = Input =
|
||||
// ==============================================================
|
||||
&-control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
|
||||
&-label > label {
|
||||
height: auto;
|
||||
&:first-child:not([class^=~"'@{ant-prefix}-col-'"]):not([class*=~"' @{ant-prefix}-col-'"]) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// fix https://github.com/vueComponent/ant-design-vue/issues/3319
|
||||
.@{form-prefix-cls}-item-control-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
&-control-input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: @input-height-base;
|
||||
|
||||
.@{form-prefix-cls}-vertical .@{form-prefix-cls}-item-label,
|
||||
// when labelCol is 24, it is a vertical form
|
||||
.@{ant-prefix}-col-24.@{form-prefix-cls}-item-label,
|
||||
.@{ant-prefix}-col-xl-24.@{form-prefix-cls}-item-label {
|
||||
.make-vertical-layout-label();
|
||||
}
|
||||
|
||||
.@{form-prefix-cls}-vertical {
|
||||
.@{form-prefix-cls}-item {
|
||||
padding-bottom: 8px;
|
||||
&-content {
|
||||
flex: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
.@{form-prefix-cls}-item-control {
|
||||
|
||||
&-explain,
|
||||
&-extra {
|
||||
clear: both;
|
||||
min-height: @form-item-margin-bottom;
|
||||
color: @text-color-secondary;
|
||||
font-size: @font-size-base;
|
||||
line-height: @line-height-base;
|
||||
}
|
||||
.@{form-prefix-cls}-explain {
|
||||
margin-top: 2px;
|
||||
margin-bottom: -4px - @form-explain-precision;
|
||||
}
|
||||
.@{form-prefix-cls}-extra {
|
||||
margin-top: 2px;
|
||||
margin-bottom: -4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: @screen-xs-max) {
|
||||
.make-vertical-layout();
|
||||
.@{ant-prefix}-col-xs-24.@{form-prefix-cls}-item-label {
|
||||
.make-vertical-layout-label();
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: @screen-sm-max) {
|
||||
.@{ant-prefix}-col-sm-24.@{form-prefix-cls}-item-label {
|
||||
.make-vertical-layout-label();
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: @screen-md-max) {
|
||||
.@{ant-prefix}-col-md-24.@{form-prefix-cls}-item-label {
|
||||
.make-vertical-layout-label();
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: @screen-lg-max) {
|
||||
.@{ant-prefix}-col-lg-24.@{form-prefix-cls}-item-label {
|
||||
.make-vertical-layout-label();
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: @screen-xl-max) {
|
||||
.@{ant-prefix}-col-xl-24.@{form-prefix-cls}-item-label {
|
||||
.make-vertical-layout-label();
|
||||
}
|
||||
}
|
||||
|
||||
//== Inline Form
|
||||
.@{form-prefix-cls}-inline {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.@{form-prefix-cls}-item {
|
||||
flex: none;
|
||||
flex-wrap: nowrap;
|
||||
margin-right: 16px;
|
||||
margin-bottom: 0;
|
||||
|
||||
&-with-help {
|
||||
margin-bottom: @form-item-margin-bottom;
|
||||
}
|
||||
|
||||
> .@{form-prefix-cls}-item-control-wrapper,
|
||||
> .@{form-prefix-cls}-item-label {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
> .@{form-prefix-cls}-item-label {
|
||||
flex: none;
|
||||
}
|
||||
transition: color 0.3s @ease-out; // sync input color transition
|
||||
.explainAndExtraDistance((@form-item-margin-bottom - @form-font-height) / 2);
|
||||
}
|
||||
|
||||
.@{form-prefix-cls}-text {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.has-feedback {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
// Validation state
|
||||
.has-success,
|
||||
.has-warning,
|
||||
.has-error,
|
||||
.is-validating {
|
||||
&.has-feedback .@{form-prefix-cls}-item-children-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
width: @form-component-height;
|
||||
height: 20px;
|
||||
margin-top: -10px;
|
||||
font-size: @form-feedback-icon-size;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
visibility: visible;
|
||||
animation: zoomIn 0.3s @ease-out-back;
|
||||
pointer-events: none;
|
||||
|
||||
& svg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.has-success {
|
||||
&.has-feedback .@{form-prefix-cls}-item-children-icon {
|
||||
color: @success-color;
|
||||
animation-name: diffZoomIn1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.has-warning {
|
||||
.form-control-validation(@warning-color; @warning-color; @form-warning-input-bg;);
|
||||
|
||||
&.has-feedback .@{form-prefix-cls}-item-children-icon {
|
||||
color: @warning-color;
|
||||
animation-name: diffZoomIn3 !important;
|
||||
}
|
||||
|
||||
//select
|
||||
.@{ant-prefix}-select {
|
||||
&-selector {
|
||||
border-color: @warning-color;
|
||||
&:hover {
|
||||
border-color: @warning-color;
|
||||
}
|
||||
}
|
||||
&-open .@{ant-prefix}-select-selector,
|
||||
&-focused .@{ant-prefix}-select-selector {
|
||||
.active(@warning-color);
|
||||
}
|
||||
}
|
||||
|
||||
// arrow and icon
|
||||
.@{ant-prefix}-calendar-picker-icon::after,
|
||||
.@{ant-prefix}-time-picker-icon::after,
|
||||
.@{ant-prefix}-picker-icon::after,
|
||||
.@{ant-prefix}-select-arrow,
|
||||
.@{ant-prefix}-cascader-picker-arrow {
|
||||
color: @warning-color;
|
||||
}
|
||||
|
||||
//input-number, timepicker
|
||||
.@{ant-prefix}-input-number,
|
||||
.@{ant-prefix}-time-picker-input {
|
||||
border-color: @warning-color;
|
||||
&-focused,
|
||||
&:focus {
|
||||
.active(@warning-color);
|
||||
}
|
||||
&:not([disabled]):hover {
|
||||
border-color: @warning-color;
|
||||
}
|
||||
}
|
||||
|
||||
.@{ant-prefix}-cascader-picker {
|
||||
&:focus .@{ant-prefix}-cascader-input {
|
||||
.active(@warning-color);
|
||||
}
|
||||
&:hover .@{ant-prefix}-cascader-input {
|
||||
border-color: @warning-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.has-error,
|
||||
&-has-error {
|
||||
.form-control-validation(@error-color; @error-color; @form-error-input-bg;);
|
||||
|
||||
&.has-feedback .@{form-prefix-cls}-item-children-icon {
|
||||
color: @error-color;
|
||||
animation-name: diffZoomIn2 !important;
|
||||
}
|
||||
|
||||
//select
|
||||
.@{ant-prefix}-select:not(.@{ant-prefix}-select-borderless) {
|
||||
.@{ant-prefix}-select-selector {
|
||||
border-color: @error-color !important;
|
||||
}
|
||||
&.@{ant-prefix}-select-open .@{ant-prefix}-select-selector,
|
||||
&.@{ant-prefix}-select-focused .@{ant-prefix}-select-selector {
|
||||
.active(@error-color);
|
||||
}
|
||||
}
|
||||
|
||||
.@{ant-prefix}-select.@{ant-prefix}-select-auto-complete {
|
||||
.@{ant-prefix}-input:focus {
|
||||
border-color: @error-color;
|
||||
}
|
||||
}
|
||||
|
||||
.@{ant-prefix}-input-group-addon .@{ant-prefix}-select {
|
||||
&-selection {
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
//input-number, timepicker
|
||||
.@{ant-prefix}-input-number,
|
||||
.@{ant-prefix}-time-picker-input {
|
||||
border-color: @error-color;
|
||||
&-focused,
|
||||
&:focus {
|
||||
.active(@error-color);
|
||||
}
|
||||
&:not([disabled]):hover {
|
||||
border-color: @error-color;
|
||||
}
|
||||
}
|
||||
.@{ant-prefix}-mention-wrapper {
|
||||
.@{ant-prefix}-mention-editor {
|
||||
&,
|
||||
&:not([disabled]):hover {
|
||||
border-color: @error-color;
|
||||
}
|
||||
}
|
||||
&.@{ant-prefix}-mention-active:not([disabled]) .@{ant-prefix}-mention-editor,
|
||||
.@{ant-prefix}-mention-editor:not([disabled]):focus {
|
||||
.active(@error-color);
|
||||
}
|
||||
}
|
||||
|
||||
.@{ant-prefix}-cascader-picker {
|
||||
&:focus .@{ant-prefix}-cascader-input {
|
||||
.active(@error-color);
|
||||
}
|
||||
&:hover .@{ant-prefix}-cascader-input {
|
||||
border-color: @error-color;
|
||||
}
|
||||
}
|
||||
|
||||
// transfer
|
||||
.@{ant-prefix}-transfer {
|
||||
&-list {
|
||||
border-color: @error-color;
|
||||
|
||||
&-search:not([disabled]) {
|
||||
border-color: @input-border-color;
|
||||
|
||||
&:hover {
|
||||
.hover();
|
||||
}
|
||||
|
||||
&:focus {
|
||||
.active();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-validating {
|
||||
&.has-feedback .@{form-prefix-cls}-item-children-icon {
|
||||
display: inline-block;
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.@{ant-prefix}-advanced-search-form {
|
||||
.@{form-prefix-cls}-item {
|
||||
margin-bottom: @form-item-margin-bottom;
|
||||
|
||||
&-with-help {
|
||||
margin-bottom: @form-item-margin-bottom - @form-explain-height - @form-help-margin-top;
|
||||
.@{ant-prefix}-input-textarea-show-count {
|
||||
&::after {
|
||||
margin-bottom: -22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.show-help-motion(@className, @keyframeName, @duration: @animation-duration-slow) {
|
||||
.make-motion(@className, @keyframeName, @duration);
|
||||
.@{className}-enter,
|
||||
.@{className}-appear {
|
||||
@name: ~'@{ant-prefix}-@{className}';
|
||||
.make-motion(@name, @keyframeName, @duration);
|
||||
.@{name}-enter,
|
||||
.@{name}-appear {
|
||||
opacity: 0;
|
||||
animation-timing-function: @ease-in-out;
|
||||
}
|
||||
.@{className}-leave {
|
||||
.@{name}-leave {
|
||||
animation-timing-function: @ease-in-out;
|
||||
}
|
||||
}
|
||||
|
@ -649,3 +258,5 @@ form {
|
|||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@import './rtl';
|
||||
|
|
|
@ -3,3 +3,4 @@ import './index.less';
|
|||
|
||||
// style dependencies
|
||||
import '../../grid/style';
|
||||
import '../../tooltip/style';
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
@import './index';
|
||||
|
||||
.@{form-prefix-cls}-inline {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.@{form-prefix-cls}-item {
|
||||
flex: none;
|
||||
flex-wrap: nowrap;
|
||||
margin-right: 16px;
|
||||
margin-bottom: 0;
|
||||
|
||||
&-with-help {
|
||||
margin-bottom: @form-item-margin-bottom;
|
||||
}
|
||||
|
||||
> .@{form-item-prefix-cls}-label,
|
||||
> .@{form-item-prefix-cls}-control {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
> .@{form-item-prefix-cls}-label {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.@{form-prefix-cls}-text {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.@{form-item-prefix-cls}-has-feedback {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,7 @@
|
|||
@import '../../input/style/mixin';
|
||||
|
||||
.form-control-validation(@text-color: @input-color; @border-color: @input-border-color; @background-color: @input-bg) {
|
||||
.@{ant-prefix}-form-explain,
|
||||
.@{ant-prefix}-form-split {
|
||||
.@{ant-prefix}-form-item-split {
|
||||
color: @text-color;
|
||||
}
|
||||
// 输入框的不同校验状态
|
||||
|
@ -10,6 +9,7 @@
|
|||
.@{ant-prefix}-input-affix-wrapper {
|
||||
&,
|
||||
&:hover {
|
||||
background-color: @background-color;
|
||||
border-color: @border-color;
|
||||
}
|
||||
|
||||
|
@ -19,18 +19,23 @@
|
|||
}
|
||||
}
|
||||
|
||||
.@{ant-prefix}-input {
|
||||
&:not(&-disabled) {
|
||||
background-color: @background-color;
|
||||
.@{ant-prefix}-input-disabled {
|
||||
&,
|
||||
&:hover {
|
||||
background-color: @input-disabled-bg;
|
||||
border-color: @input-border-color;
|
||||
}
|
||||
}
|
||||
|
||||
.@{ant-prefix}-input-affix-wrapper {
|
||||
&:not(&-disabled) {
|
||||
background-color: @background-color;
|
||||
}
|
||||
input:focus {
|
||||
box-shadow: none !important;
|
||||
.@{ant-prefix}-input-affix-wrapper-disabled {
|
||||
&,
|
||||
&:hover {
|
||||
background-color: @input-disabled-bg;
|
||||
border-color: @input-border-color;
|
||||
|
||||
input:focus {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,185 @@
|
|||
@import '../../style/themes/index';
|
||||
@import '../../style/mixins/index';
|
||||
@import '../../input/style/mixin';
|
||||
@import '../../button/style/mixin';
|
||||
@import '../../grid/style/mixin';
|
||||
|
||||
@form-prefix-cls: ~'@{ant-prefix}-form';
|
||||
@form-item-prefix-cls: ~'@{form-prefix-cls}-item';
|
||||
|
||||
.@{form-prefix-cls} {
|
||||
&-rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// = Item =
|
||||
// ================================================================
|
||||
.@{form-item-prefix-cls} {
|
||||
// ==============================================================
|
||||
// = Label =
|
||||
// ==============================================================
|
||||
&-label {
|
||||
.@{form-prefix-cls}-rtl & {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
> label {
|
||||
&.@{form-item-prefix-cls}-required::before {
|
||||
.@{form-prefix-cls}-rtl & {
|
||||
margin-right: 0;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
&::after {
|
||||
.@{form-prefix-cls}-rtl & {
|
||||
margin: 0 @form-item-label-colon-margin-left 0 @form-item-label-colon-margin-right;
|
||||
}
|
||||
}
|
||||
|
||||
.@{form-item-prefix-cls}-optional {
|
||||
.@{form-prefix-cls}-rtl & {
|
||||
margin-right: @margin-xss;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================================
|
||||
// = Input =
|
||||
// ==============================================================
|
||||
&-control {
|
||||
.@{ant-prefix}-col-rtl &:first-child {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// status
|
||||
&-has-feedback {
|
||||
.@{ant-prefix}-input {
|
||||
.@{form-prefix-cls}-rtl & {
|
||||
padding-right: @input-padding-horizontal-base;
|
||||
padding-left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.@{ant-prefix}-input-affix-wrapper {
|
||||
.@{ant-prefix}-input-suffix {
|
||||
.@{form-prefix-cls}-rtl & {
|
||||
padding-right: @input-padding-horizontal-base;
|
||||
padding-left: 18px;
|
||||
}
|
||||
}
|
||||
.@{ant-prefix}-input {
|
||||
.@{form-prefix-cls}-rtl & {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{ant-prefix}-input-search:not(.@{ant-prefix}-input-search-enter-button) {
|
||||
.@{ant-prefix}-input-suffix {
|
||||
.@{form-prefix-cls}-rtl & {
|
||||
right: auto;
|
||||
left: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{ant-prefix}-input-number {
|
||||
.@{form-prefix-cls}-rtl & {
|
||||
padding-left: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
> .@{ant-prefix}-select .@{ant-prefix}-select-arrow,
|
||||
> .@{ant-prefix}-select .@{ant-prefix}-select-clear,
|
||||
:not(.@{ant-prefix}-input-group-addon) > .@{ant-prefix}-select .@{ant-prefix}-select-arrow,
|
||||
:not(.@{ant-prefix}-input-group-addon) > .@{ant-prefix}-select .@{ant-prefix}-select-clear {
|
||||
.@{form-prefix-cls}-rtl & {
|
||||
right: auto;
|
||||
left: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
> .@{ant-prefix}-select .@{ant-prefix}-select-selection-selected-value,
|
||||
:not(.@{ant-prefix}-input-group-addon)
|
||||
> .@{ant-prefix}-select
|
||||
.@{ant-prefix}-select-selection-selected-value {
|
||||
.@{form-prefix-cls}-rtl & {
|
||||
padding-right: 0;
|
||||
padding-left: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
.@{ant-prefix}-cascader-picker {
|
||||
&-arrow {
|
||||
.@{form-prefix-cls}-rtl & {
|
||||
margin-right: 0;
|
||||
margin-left: 19px;
|
||||
}
|
||||
}
|
||||
&-clear {
|
||||
.@{form-prefix-cls}-rtl & {
|
||||
right: auto;
|
||||
left: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{ant-prefix}-picker {
|
||||
.@{form-prefix-cls}-rtl & {
|
||||
padding-right: @input-padding-horizontal-base;
|
||||
padding-left: @input-padding-horizontal-base + @font-size-base * 1.3;
|
||||
}
|
||||
|
||||
&-large {
|
||||
.@{form-prefix-cls}-rtl & {
|
||||
padding-right: @input-padding-horizontal-lg;
|
||||
padding-left: @input-padding-horizontal-lg + @font-size-base * 1.3;
|
||||
}
|
||||
}
|
||||
|
||||
&-small {
|
||||
.@{form-prefix-cls}-rtl & {
|
||||
padding-right: @input-padding-horizontal-sm;
|
||||
padding-left: @input-padding-horizontal-sm + @font-size-base * 1.3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.@{form-item-prefix-cls} {
|
||||
&-has-success,
|
||||
&-has-warning,
|
||||
&-has-error,
|
||||
&-is-validating {
|
||||
// ====================== Icon ======================
|
||||
.@{form-item-prefix-cls}-children-icon {
|
||||
.@{form-prefix-cls}-rtl & {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// inline
|
||||
.@{form-prefix-cls}-inline {
|
||||
.@{form-prefix-cls}-item {
|
||||
.@{form-prefix-cls}-rtl& {
|
||||
margin-right: 0;
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// vertical
|
||||
.make-vertical-layout-label() {
|
||||
.@{form-prefix-cls}-rtl& {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,278 @@
|
|||
@import './index.less';
|
||||
|
||||
.@{form-item-prefix-cls} {
|
||||
// ================================================================
|
||||
// = Status =
|
||||
// ================================================================
|
||||
/* Some non-status related component style is in `components.less` */
|
||||
|
||||
// ========================= Explain =========================
|
||||
/* To support leave along ErrorList. We add additional className to handle explain style */
|
||||
&-explain {
|
||||
&&-error {
|
||||
color: @error-color;
|
||||
}
|
||||
|
||||
&&-warning {
|
||||
color: @warning-color;
|
||||
}
|
||||
}
|
||||
|
||||
&-has-feedback {
|
||||
// ========================= Input =========================
|
||||
.@{ant-prefix}-input {
|
||||
padding-right: 24px;
|
||||
}
|
||||
// https://github.com/ant-design/ant-design/issues/19884
|
||||
.@{ant-prefix}-input-affix-wrapper {
|
||||
.@{ant-prefix}-input-suffix {
|
||||
padding-right: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
// Fix issue: https://github.com/ant-design/ant-design/issues/7854
|
||||
.@{ant-prefix}-input-search:not(.@{ant-prefix}-input-search-enter-button) {
|
||||
.@{ant-prefix}-input-suffix {
|
||||
right: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== Switch =========================
|
||||
.@{ant-prefix}-switch {
|
||||
margin: 2px 0 4px;
|
||||
}
|
||||
|
||||
// ======================== Select =========================
|
||||
// Fix overlapping between feedback icon and <Select>'s arrow.
|
||||
// https://github.com/ant-design/ant-design/issues/4431
|
||||
> .@{ant-prefix}-select .@{ant-prefix}-select-arrow,
|
||||
> .@{ant-prefix}-select .@{ant-prefix}-select-clear,
|
||||
:not(.@{ant-prefix}-input-group-addon) > .@{ant-prefix}-select .@{ant-prefix}-select-arrow,
|
||||
:not(.@{ant-prefix}-input-group-addon) > .@{ant-prefix}-select .@{ant-prefix}-select-clear {
|
||||
right: 32px;
|
||||
}
|
||||
> .@{ant-prefix}-select .@{ant-prefix}-select-selection-selected-value,
|
||||
:not(.@{ant-prefix}-input-group-addon)
|
||||
> .@{ant-prefix}-select
|
||||
.@{ant-prefix}-select-selection-selected-value {
|
||||
padding-right: 42px;
|
||||
}
|
||||
|
||||
// ======================= Cascader ========================
|
||||
.@{ant-prefix}-cascader-picker {
|
||||
&-arrow {
|
||||
margin-right: 19px;
|
||||
}
|
||||
&-clear {
|
||||
right: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== Picker =========================
|
||||
// Fix issue: https://github.com/ant-design/ant-design/issues/4783
|
||||
.@{ant-prefix}-picker {
|
||||
padding-right: @input-padding-horizontal-base + @font-size-base * 1.3;
|
||||
|
||||
&-large {
|
||||
padding-right: @input-padding-horizontal-lg + @font-size-base * 1.3;
|
||||
}
|
||||
|
||||
&-small {
|
||||
padding-right: @input-padding-horizontal-sm + @font-size-base * 1.3;
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== Status Group ======================
|
||||
&.@{form-item-prefix-cls} {
|
||||
&-has-success,
|
||||
&-has-warning,
|
||||
&-has-error,
|
||||
&-is-validating {
|
||||
// ====================== Icon ======================
|
||||
.@{form-item-prefix-cls}-children-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
width: @input-height-base;
|
||||
height: 20px;
|
||||
margin-top: -10px;
|
||||
font-size: @font-size-base;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
visibility: visible;
|
||||
animation: zoomIn 0.3s @ease-out-back;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== Success ========================
|
||||
&-has-success {
|
||||
&.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon {
|
||||
color: @success-color;
|
||||
animation-name: diffZoomIn1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== Warning ========================
|
||||
&-has-warning {
|
||||
.form-control-validation(@warning-color; @warning-color; @form-warning-input-bg);
|
||||
|
||||
&.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon {
|
||||
color: @warning-color;
|
||||
animation-name: diffZoomIn3 !important;
|
||||
}
|
||||
|
||||
// Select
|
||||
.@{ant-prefix}-select:not(.@{ant-prefix}-select-disabled):not(.@{ant-prefix}-select-customize-input) {
|
||||
.@{ant-prefix}-select-selector {
|
||||
background-color: @form-warning-input-bg;
|
||||
border-color: @warning-color !important;
|
||||
}
|
||||
&.@{ant-prefix}-select-open .@{ant-prefix}-select-selector,
|
||||
&.@{ant-prefix}-select-focused .@{ant-prefix}-select-selector {
|
||||
.active(@warning-color);
|
||||
}
|
||||
}
|
||||
|
||||
// InputNumber, TimePicker
|
||||
.@{ant-prefix}-input-number,
|
||||
.@{ant-prefix}-picker {
|
||||
background-color: @form-warning-input-bg;
|
||||
border-color: @warning-color;
|
||||
&-focused,
|
||||
&:focus {
|
||||
.active(@warning-color);
|
||||
}
|
||||
&:not([disabled]):hover {
|
||||
background-color: @form-warning-input-bg;
|
||||
border-color: @warning-color;
|
||||
}
|
||||
}
|
||||
|
||||
.@{ant-prefix}-cascader-picker:focus .@{ant-prefix}-cascader-input {
|
||||
.active(@warning-color);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================= Error =========================
|
||||
&-has-error {
|
||||
.form-control-validation(@error-color; @error-color; @form-error-input-bg);
|
||||
|
||||
&.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon {
|
||||
color: @error-color;
|
||||
animation-name: diffZoomIn2 !important;
|
||||
}
|
||||
|
||||
// Select
|
||||
.@{ant-prefix}-select:not(.@{ant-prefix}-select-disabled):not(.@{ant-prefix}-select-customize-input) {
|
||||
.@{ant-prefix}-select-selector {
|
||||
background-color: @form-error-input-bg;
|
||||
border-color: @error-color !important;
|
||||
}
|
||||
&.@{ant-prefix}-select-open .@{ant-prefix}-select-selector,
|
||||
&.@{ant-prefix}-select-focused .@{ant-prefix}-select-selector {
|
||||
.active(@error-color);
|
||||
}
|
||||
}
|
||||
|
||||
// fixes https://github.com/ant-design/ant-design/issues/20482
|
||||
.@{ant-prefix}-input-group-addon .@{ant-prefix}-select {
|
||||
&.@{ant-prefix}-select-single:not(.@{ant-prefix}-select-customize-input)
|
||||
.@{ant-prefix}-select-selector {
|
||||
background-color: inherit;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.@{ant-prefix}-select.@{ant-prefix}-select-auto-complete {
|
||||
.@{ant-prefix}-input:focus {
|
||||
border-color: @error-color;
|
||||
}
|
||||
}
|
||||
|
||||
// InputNumber, TimePicker
|
||||
.@{ant-prefix}-input-number,
|
||||
.@{ant-prefix}-picker {
|
||||
background-color: @form-error-input-bg;
|
||||
border-color: @error-color;
|
||||
&-focused,
|
||||
&:focus {
|
||||
.active(@error-color);
|
||||
}
|
||||
&:not([disabled]):hover {
|
||||
background-color: @form-error-input-bg;
|
||||
border-color: @error-color;
|
||||
}
|
||||
}
|
||||
|
||||
.@{ant-prefix}-mention-wrapper {
|
||||
.@{ant-prefix}-mention-editor {
|
||||
&,
|
||||
&:not([disabled]):hover {
|
||||
background-color: @form-error-input-bg;
|
||||
border-color: @error-color;
|
||||
}
|
||||
}
|
||||
&.@{ant-prefix}-mention-active:not([disabled]) .@{ant-prefix}-mention-editor,
|
||||
.@{ant-prefix}-mention-editor:not([disabled]):focus {
|
||||
.active(@error-color);
|
||||
}
|
||||
}
|
||||
|
||||
// cascader
|
||||
.@{ant-prefix}-cascader-picker {
|
||||
&:hover
|
||||
.@{ant-prefix}-cascader-picker-label:hover
|
||||
+ .@{ant-prefix}-cascader-input.@{ant-prefix}-input {
|
||||
border-color: @error-color;
|
||||
}
|
||||
|
||||
&:focus .@{ant-prefix}-cascader-input {
|
||||
background-color: @form-error-input-bg;
|
||||
.active(@error-color);
|
||||
}
|
||||
}
|
||||
|
||||
// transfer
|
||||
.@{ant-prefix}-transfer {
|
||||
&-list {
|
||||
border-color: @error-color;
|
||||
|
||||
&-search:not([disabled]) {
|
||||
border-color: @input-border-color;
|
||||
|
||||
&:hover {
|
||||
.hover();
|
||||
}
|
||||
|
||||
&:focus {
|
||||
.active();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RadioGroup
|
||||
.@{ant-prefix}-radio-button-wrapper {
|
||||
border-color: @error-color !important;
|
||||
|
||||
&:not(:first-child) {
|
||||
&::before {
|
||||
background-color: @error-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== Validating =======================
|
||||
&-is-validating {
|
||||
&.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon {
|
||||
display: inline-block;
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
@import './index';
|
||||
|
||||
// ================== Label ==================
|
||||
.make-vertical-layout-label() {
|
||||
& when (@form-vertical-label-margin > 0) {
|
||||
margin: @form-vertical-label-margin;
|
||||
}
|
||||
padding: @form-vertical-label-padding;
|
||||
line-height: @line-height-base;
|
||||
white-space: initial;
|
||||
text-align: left;
|
||||
|
||||
> label {
|
||||
margin: 0;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.make-vertical-layout() {
|
||||
.@{form-prefix-cls}-item .@{form-prefix-cls}-item-label {
|
||||
.make-vertical-layout-label();
|
||||
}
|
||||
.@{form-prefix-cls} {
|
||||
.@{form-prefix-cls}-item {
|
||||
flex-wrap: wrap;
|
||||
.@{form-prefix-cls}-item-label,
|
||||
.@{form-prefix-cls}-item-control {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{form-prefix-cls}-vertical {
|
||||
.@{form-item-prefix-cls} {
|
||||
flex-direction: column;
|
||||
|
||||
&-label > label {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{form-prefix-cls}-vertical .@{form-item-prefix-cls}-label,
|
||||
// when labelCol is 24, it is a vertical form
|
||||
.@{ant-prefix}-col-24.@{form-item-prefix-cls}-label,
|
||||
.@{ant-prefix}-col-xl-24.@{form-item-prefix-cls}-label {
|
||||
.make-vertical-layout-label();
|
||||
}
|
||||
|
||||
@media (max-width: @screen-xs-max) {
|
||||
.make-vertical-layout();
|
||||
.@{ant-prefix}-col-xs-24.@{form-item-prefix-cls}-label {
|
||||
.make-vertical-layout-label();
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: @screen-sm-max) {
|
||||
.@{ant-prefix}-col-sm-24.@{form-item-prefix-cls}-label {
|
||||
.make-vertical-layout-label();
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: @screen-md-max) {
|
||||
.@{ant-prefix}-col-md-24.@{form-item-prefix-cls}-label {
|
||||
.make-vertical-layout-label();
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: @screen-lg-max) {
|
||||
.@{ant-prefix}-col-lg-24.@{form-item-prefix-cls}-label {
|
||||
.make-vertical-layout-label();
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: @screen-xl-max) {
|
||||
.@{ant-prefix}-col-xl-24.@{form-item-prefix-cls}-label {
|
||||
.make-vertical-layout-label();
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import { inject, defineComponent, HTMLAttributes, CSSProperties } from 'vue';
|
||||
import { defineComponent, CSSProperties, ExtractPropTypes, computed } from 'vue';
|
||||
import classNames from '../_util/classNames';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import { defaultConfigProvider } from '../config-provider';
|
||||
import { rowContextState } from './Row';
|
||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||
import { useInjectRow } from './context';
|
||||
|
||||
type ColSpanType = number | string;
|
||||
|
||||
|
@ -16,22 +16,6 @@ export interface ColSize {
|
|||
pull?: ColSpanType;
|
||||
}
|
||||
|
||||
export interface ColProps extends HTMLAttributes {
|
||||
span?: ColSpanType;
|
||||
order?: ColSpanType;
|
||||
offset?: ColSpanType;
|
||||
push?: ColSpanType;
|
||||
pull?: ColSpanType;
|
||||
xs?: ColSpanType | ColSize;
|
||||
sm?: ColSpanType | ColSize;
|
||||
md?: ColSpanType | ColSize;
|
||||
lg?: ColSpanType | ColSize;
|
||||
xl?: ColSpanType | ColSize;
|
||||
xxl?: ColSpanType | ColSize;
|
||||
prefixCls?: string;
|
||||
flex?: FlexType;
|
||||
}
|
||||
|
||||
function parseFlex(flex: FlexType): string {
|
||||
if (typeof flex === 'number') {
|
||||
return `${flex} ${flex} auto`;
|
||||
|
@ -44,92 +28,17 @@ function parseFlex(flex: FlexType): string {
|
|||
return flex;
|
||||
}
|
||||
|
||||
const ACol = defineComponent<ColProps>({
|
||||
name: 'ACol',
|
||||
setup(props, { slots }) {
|
||||
const configProvider = inject('configProvider', defaultConfigProvider);
|
||||
const rowContext = inject<rowContextState>('rowContext', {});
|
||||
|
||||
return () => {
|
||||
const { gutter } = rowContext;
|
||||
const { prefixCls: customizePrefixCls, span, order, offset, push, pull, flex } = props;
|
||||
const prefixCls = configProvider.getPrefixCls('col', customizePrefixCls);
|
||||
let sizeClassObj = {};
|
||||
['xs', 'sm', 'md', 'lg', 'xl', 'xxl'].forEach(size => {
|
||||
let sizeProps: ColSize = {};
|
||||
const propSize = props[size];
|
||||
if (typeof propSize === 'number') {
|
||||
sizeProps.span = propSize;
|
||||
} else if (typeof propSize === 'object') {
|
||||
sizeProps = propSize || {};
|
||||
}
|
||||
|
||||
sizeClassObj = {
|
||||
...sizeClassObj,
|
||||
[`${prefixCls}-${size}-${sizeProps.span}`]: sizeProps.span !== undefined,
|
||||
[`${prefixCls}-${size}-order-${sizeProps.order}`]:
|
||||
sizeProps.order || sizeProps.order === 0,
|
||||
[`${prefixCls}-${size}-offset-${sizeProps.offset}`]:
|
||||
sizeProps.offset || sizeProps.offset === 0,
|
||||
[`${prefixCls}-${size}-push-${sizeProps.push}`]: sizeProps.push || sizeProps.push === 0,
|
||||
[`${prefixCls}-${size}-pull-${sizeProps.pull}`]: sizeProps.pull || sizeProps.pull === 0,
|
||||
};
|
||||
});
|
||||
const classes = classNames(
|
||||
prefixCls,
|
||||
{
|
||||
[`${prefixCls}-${span}`]: span !== undefined,
|
||||
[`${prefixCls}-order-${order}`]: order,
|
||||
[`${prefixCls}-offset-${offset}`]: offset,
|
||||
[`${prefixCls}-push-${push}`]: push,
|
||||
[`${prefixCls}-pull-${pull}`]: pull,
|
||||
},
|
||||
sizeClassObj,
|
||||
);
|
||||
let mergedStyle: CSSProperties = {};
|
||||
if (gutter) {
|
||||
mergedStyle = {
|
||||
...(gutter[0] > 0
|
||||
? {
|
||||
paddingLeft: `${gutter[0] / 2}px`,
|
||||
paddingRight: `${gutter[0] / 2}px`,
|
||||
}
|
||||
: {}),
|
||||
...(gutter[1] > 0
|
||||
? {
|
||||
paddingTop: `${gutter[1] / 2}px`,
|
||||
paddingBottom: `${gutter[1] / 2}px`,
|
||||
}
|
||||
: {}),
|
||||
...mergedStyle,
|
||||
};
|
||||
}
|
||||
if (flex) {
|
||||
mergedStyle.flex = parseFlex(flex);
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={classes} style={mergedStyle}>
|
||||
{slots.default?.()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const stringOrNumber = PropTypes.oneOfType([PropTypes.string, PropTypes.number]);
|
||||
|
||||
export const ColSize = PropTypes.shape({
|
||||
export const colSize = PropTypes.shape({
|
||||
span: stringOrNumber,
|
||||
order: stringOrNumber,
|
||||
offset: stringOrNumber,
|
||||
push: stringOrNumber,
|
||||
pull: stringOrNumber,
|
||||
}).loose;
|
||||
const objectOrNumber = PropTypes.oneOfType([PropTypes.string, PropTypes.number, colSize]);
|
||||
|
||||
const objectOrNumber = PropTypes.oneOfType([PropTypes.string, PropTypes.number, ColSize]);
|
||||
|
||||
ACol.props = {
|
||||
const colProps = {
|
||||
span: stringOrNumber,
|
||||
order: stringOrNumber,
|
||||
offset: stringOrNumber,
|
||||
|
@ -145,4 +54,85 @@ ACol.props = {
|
|||
flex: stringOrNumber,
|
||||
};
|
||||
|
||||
export default ACol;
|
||||
export type ColProps = Partial<ExtractPropTypes<typeof colProps>>;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ACol',
|
||||
props: colProps,
|
||||
setup(props, { slots }) {
|
||||
const { gutter, supportFlexGap, wrap } = useInjectRow();
|
||||
const { prefixCls, direction } = useConfigInject('col', props);
|
||||
const classes = computed(() => {
|
||||
const { span, order, offset, push, pull } = props;
|
||||
const pre = prefixCls.value;
|
||||
let sizeClassObj = {};
|
||||
['xs', 'sm', 'md', 'lg', 'xl', 'xxl'].forEach(size => {
|
||||
let sizeProps: ColSize = {};
|
||||
const propSize = props[size];
|
||||
if (typeof propSize === 'number') {
|
||||
sizeProps.span = propSize;
|
||||
} else if (typeof propSize === 'object') {
|
||||
sizeProps = propSize || {};
|
||||
}
|
||||
|
||||
sizeClassObj = {
|
||||
...sizeClassObj,
|
||||
[`${pre}-${size}-${sizeProps.span}`]: sizeProps.span !== undefined,
|
||||
[`${pre}-${size}-order-${sizeProps.order}`]: sizeProps.order || sizeProps.order === 0,
|
||||
[`${pre}-${size}-offset-${sizeProps.offset}`]: sizeProps.offset || sizeProps.offset === 0,
|
||||
[`${pre}-${size}-push-${sizeProps.push}`]: sizeProps.push || sizeProps.push === 0,
|
||||
[`${pre}-${size}-pull-${sizeProps.pull}`]: sizeProps.pull || sizeProps.pull === 0,
|
||||
[`${pre}-rtl`]: direction.value === 'rtl',
|
||||
};
|
||||
});
|
||||
return classNames(
|
||||
pre,
|
||||
{
|
||||
[`${pre}-${span}`]: span !== undefined,
|
||||
[`${pre}-order-${order}`]: order,
|
||||
[`${pre}-offset-${offset}`]: offset,
|
||||
[`${pre}-push-${push}`]: push,
|
||||
[`${pre}-pull-${pull}`]: pull,
|
||||
},
|
||||
sizeClassObj,
|
||||
);
|
||||
});
|
||||
|
||||
const mergedStyle = computed(() => {
|
||||
const { flex } = props;
|
||||
const gutterVal = gutter.value;
|
||||
const style: CSSProperties = {};
|
||||
// Horizontal gutter use padding
|
||||
if (gutterVal && gutterVal[0] > 0) {
|
||||
const horizontalGutter = `${gutterVal[0] / 2}px`;
|
||||
style.paddingLeft = horizontalGutter;
|
||||
style.paddingRight = horizontalGutter;
|
||||
}
|
||||
|
||||
// Vertical gutter use padding when gap not support
|
||||
if (gutterVal && gutterVal[1] > 0 && !supportFlexGap.value) {
|
||||
const verticalGutter = `${gutterVal[1] / 2}px`;
|
||||
style.paddingTop = verticalGutter;
|
||||
style.paddingBottom = verticalGutter;
|
||||
}
|
||||
|
||||
if (flex) {
|
||||
style.flex = parseFlex(flex);
|
||||
|
||||
// Hack for Firefox to avoid size issue
|
||||
// https://github.com/ant-design/ant-design/pull/20023#issuecomment-564389553
|
||||
if (flex === 'auto' && wrap.value === false && !style.minWidth) {
|
||||
style.minWidth = 0;
|
||||
}
|
||||
}
|
||||
return style;
|
||||
});
|
||||
return () => {
|
||||
return (
|
||||
<div class={classes.value} style={mergedStyle.value}>
|
||||
{slots.default?.()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
import {
|
||||
inject,
|
||||
provide,
|
||||
reactive,
|
||||
defineComponent,
|
||||
HTMLAttributes,
|
||||
ref,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
ExtractPropTypes,
|
||||
computed,
|
||||
CSSProperties,
|
||||
} from 'vue';
|
||||
import classNames from '../_util/classNames';
|
||||
import { tuple } from '../_util/type';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import { defaultConfigProvider } from '../config-provider';
|
||||
import ResponsiveObserve, {
|
||||
Breakpoint,
|
||||
ScreenMap,
|
||||
responsiveArray,
|
||||
} from '../_util/responsiveObserve';
|
||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||
import useFlexGapSupport from '../_util/hooks/useFlexGapSupport';
|
||||
import useProvideRow from './context';
|
||||
|
||||
const RowAligns = tuple('top', 'middle', 'bottom', 'stretch');
|
||||
const RowJustify = tuple('start', 'end', 'center', 'space-around', 'space-between');
|
||||
|
@ -27,24 +28,36 @@ export interface rowContextState {
|
|||
gutter?: [number, number];
|
||||
}
|
||||
|
||||
export interface RowProps extends HTMLAttributes {
|
||||
type?: 'flex';
|
||||
gutter?: Gutter | [Gutter, Gutter];
|
||||
align?: typeof RowAligns[number];
|
||||
justify?: typeof RowJustify[number];
|
||||
prefixCls?: string;
|
||||
}
|
||||
const rowProps = {
|
||||
type: PropTypes.oneOf(['flex']),
|
||||
align: PropTypes.oneOf(RowAligns),
|
||||
justify: PropTypes.oneOf(RowJustify),
|
||||
prefixCls: PropTypes.string,
|
||||
gutter: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]).def(0),
|
||||
wrap: PropTypes.looseBool,
|
||||
};
|
||||
|
||||
const ARow = defineComponent<RowProps>({
|
||||
export type RowProps = Partial<ExtractPropTypes<typeof rowProps>>;
|
||||
|
||||
const ARow = defineComponent({
|
||||
name: 'ARow',
|
||||
props: rowProps,
|
||||
setup(props, { slots }) {
|
||||
const rowContext = reactive<rowContextState>({
|
||||
gutter: undefined,
|
||||
});
|
||||
provide('rowContext', rowContext);
|
||||
const { prefixCls, direction } = useConfigInject('row', props);
|
||||
|
||||
let token: number;
|
||||
|
||||
const screens = ref<ScreenMap>({
|
||||
xs: true,
|
||||
sm: true,
|
||||
md: true,
|
||||
lg: true,
|
||||
xl: true,
|
||||
xxl: true,
|
||||
});
|
||||
|
||||
const supportFlexGap = useFlexGapSupport();
|
||||
|
||||
onMounted(() => {
|
||||
token = ResponsiveObserve.subscribe(screen => {
|
||||
const currentGutter = props.gutter || 0;
|
||||
|
@ -62,18 +75,7 @@ const ARow = defineComponent<RowProps>({
|
|||
ResponsiveObserve.unsubscribe(token);
|
||||
});
|
||||
|
||||
const screens = ref<ScreenMap>({
|
||||
xs: true,
|
||||
sm: true,
|
||||
md: true,
|
||||
lg: true,
|
||||
xl: true,
|
||||
xxl: true,
|
||||
});
|
||||
|
||||
const { getPrefixCls } = inject('configProvider', defaultConfigProvider);
|
||||
|
||||
const getGutter = (): [number, number] => {
|
||||
const gutter = computed(() => {
|
||||
const results: [number, number] = [0, 0];
|
||||
const { gutter = 0 } = props;
|
||||
const normalizedGutter = Array.isArray(gutter) ? gutter : [gutter, 0];
|
||||
|
@ -91,34 +93,48 @@ const ARow = defineComponent<RowProps>({
|
|||
}
|
||||
});
|
||||
return results;
|
||||
};
|
||||
});
|
||||
|
||||
useProvideRow({
|
||||
gutter,
|
||||
supportFlexGap,
|
||||
wrap: computed(() => props.wrap),
|
||||
});
|
||||
|
||||
const classes = computed(() =>
|
||||
classNames(prefixCls.value, {
|
||||
[`${prefixCls.value}-no-wrap`]: props.wrap === false,
|
||||
[`${prefixCls.value}-${props.justify}`]: props.justify,
|
||||
[`${prefixCls.value}-${props.align}`]: props.align,
|
||||
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
|
||||
}),
|
||||
);
|
||||
|
||||
const rowStyle = computed(() => {
|
||||
const gt = gutter.value;
|
||||
// Add gutter related style
|
||||
const style: CSSProperties = {};
|
||||
const horizontalGutter = gt[0] > 0 ? `${gt[0] / -2}px` : undefined;
|
||||
const verticalGutter = gt[1] > 0 ? `${gt[1] / -2}px` : undefined;
|
||||
|
||||
if (horizontalGutter) {
|
||||
style.marginLeft = horizontalGutter;
|
||||
style.marginRight = horizontalGutter;
|
||||
}
|
||||
|
||||
if (supportFlexGap.value) {
|
||||
// Set gap direct if flex gap support
|
||||
style.rowGap = `${gt[1]}px`;
|
||||
} else if (verticalGutter) {
|
||||
style.marginTop = verticalGutter;
|
||||
style.marginBottom = verticalGutter;
|
||||
}
|
||||
return style;
|
||||
});
|
||||
|
||||
return () => {
|
||||
const { prefixCls: customizePrefixCls, justify, align } = props;
|
||||
const prefixCls = getPrefixCls('row', customizePrefixCls);
|
||||
const gutter = getGutter();
|
||||
const classes = classNames(prefixCls, {
|
||||
[`${prefixCls}-${justify}`]: justify,
|
||||
[`${prefixCls}-${align}`]: align,
|
||||
});
|
||||
const rowStyle = {
|
||||
...(gutter[0] > 0
|
||||
? {
|
||||
marginLeft: `${gutter[0] / -2}px`,
|
||||
marginRight: `${gutter[0] / -2}px`,
|
||||
}
|
||||
: {}),
|
||||
...(gutter[1] > 0
|
||||
? {
|
||||
marginTop: `${gutter[1] / -2}px`,
|
||||
marginBottom: `${gutter[1] / -2}px`,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
rowContext.gutter = gutter;
|
||||
return (
|
||||
<div class={classes} style={rowStyle}>
|
||||
<div class={classes.value} style={rowStyle.value}>
|
||||
{slots.default?.()}
|
||||
</div>
|
||||
);
|
||||
|
@ -126,12 +142,4 @@ const ARow = defineComponent<RowProps>({
|
|||
},
|
||||
});
|
||||
|
||||
ARow.props = {
|
||||
type: PropTypes.oneOf(['flex']),
|
||||
align: PropTypes.oneOf(RowAligns),
|
||||
justify: PropTypes.oneOf(RowJustify),
|
||||
prefixCls: PropTypes.string,
|
||||
gutter: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]).def(0),
|
||||
};
|
||||
|
||||
export default ARow;
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import { Ref, inject, InjectionKey, provide, ComputedRef } from 'vue';
|
||||
|
||||
export interface RowContext {
|
||||
gutter: ComputedRef<[number, number]>;
|
||||
wrap: ComputedRef<boolean>;
|
||||
supportFlexGap: Ref<boolean>;
|
||||
}
|
||||
|
||||
export const RowContextKey: InjectionKey<RowContext> = Symbol('rowContextKey');
|
||||
|
||||
const useProvideRow = (state: RowContext) => {
|
||||
provide(RowContextKey, state);
|
||||
};
|
||||
|
||||
const useInjectRow = () => {
|
||||
return inject(RowContextKey);
|
||||
};
|
||||
|
||||
export { useInjectRow, useProvideRow };
|
||||
export default useProvideRow;
|
|
@ -1,4 +1,11 @@
|
|||
import Row from './Row';
|
||||
import Col from './Col';
|
||||
import useBreakpoint from '../_util/hooks/useBreakpoint';
|
||||
|
||||
export { RowProps } from './Row';
|
||||
|
||||
export { ColProps, ColSize } from './Col';
|
||||
|
||||
export { Row, Col };
|
||||
|
||||
export default { useBreakpoint };
|
||||
|
|
|
@ -11,6 +11,11 @@
|
|||
&::after {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
// No wrap of flex
|
||||
&-no-wrap {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
// x轴原点
|
||||
|
|
|
@ -15,7 +15,7 @@ const inputNumberProps = {
|
|||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
step: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).def(1),
|
||||
defaultValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
tabindex: PropTypes.number,
|
||||
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
disabled: PropTypes.looseBool,
|
||||
size: PropTypes.oneOf(tuple('large', 'small', 'default')),
|
||||
formatter: PropTypes.func,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { defineComponent, inject } from 'vue';
|
||||
import classNames from '../_util/classNames';
|
||||
import isMobile from '../vc-menu/utils/isMobile';
|
||||
import isMobile from '../_util/isMobile';
|
||||
import Input from './Input';
|
||||
import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined';
|
||||
import SearchOutlined from '@ant-design/icons-vue/SearchOutlined';
|
||||
|
|
|
@ -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.value && zeroWidthTrigger) ? triggerDom : null}
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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');
|
|
@ -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?.());
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
@import '../../style/themes/index';
|
||||
@import '../../style/mixins/index';
|
||||
|
||||
@layout-prefix-cls: ~'@{ant-prefix}-layout';
|
||||
|
||||
.@{layout-prefix-cls} {
|
||||
&-rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { inject, defineComponent, VNodeTypes, PropType } from 'vue';
|
||||
import { inject, defineComponent, VNodeTypes, PropType, computed, ComputedRef } from 'vue';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import defaultLocaleData from './default';
|
||||
import { Locale } from '.';
|
||||
|
@ -30,39 +30,54 @@ export default defineComponent({
|
|||
>,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
localeData: inject<LocaleReceiverContext>('localeData', {}),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getLocale() {
|
||||
const { componentName = 'global', defaultLocale } = this;
|
||||
setup(props, { slots }) {
|
||||
const localeData = inject<LocaleReceiverContext>('localeData', {});
|
||||
const locale = computed(() => {
|
||||
const { componentName = 'global', defaultLocale } = props;
|
||||
const locale =
|
||||
defaultLocale || (defaultLocaleData as LocaleInterface)[componentName || 'global'];
|
||||
const { antLocale } = this.localeData;
|
||||
const { antLocale } = localeData;
|
||||
|
||||
const localeFromContext = componentName && antLocale ? antLocale[componentName] : {};
|
||||
return {
|
||||
...(typeof locale === 'function' ? locale() : locale),
|
||||
...(localeFromContext || {}),
|
||||
};
|
||||
},
|
||||
|
||||
getLocaleCode() {
|
||||
const { antLocale } = this.localeData;
|
||||
});
|
||||
const localeCode = computed(() => {
|
||||
const { antLocale } = localeData;
|
||||
const localeCode = antLocale && antLocale.locale;
|
||||
// Had use LocaleProvide but didn't set locale
|
||||
if (antLocale && antLocale.exist && !localeCode) {
|
||||
return defaultLocaleData.locale;
|
||||
}
|
||||
return localeCode;
|
||||
},
|
||||
},
|
||||
render() {
|
||||
const { $slots } = this;
|
||||
const children = this.children || $slots.default;
|
||||
const { antLocale } = this.localeData;
|
||||
return children?.(this.getLocale(), this.getLocaleCode(), antLocale);
|
||||
});
|
||||
return () => {
|
||||
const children = props.children || slots.default;
|
||||
const { antLocale } = localeData;
|
||||
return children?.(locale.value, localeCode.value, antLocale);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
type LocaleComponent = keyof Locale;
|
||||
|
||||
export function useLocaleReceiver<T extends LocaleComponent>(
|
||||
componentName: T,
|
||||
defaultLocale?: Locale[T] | Function,
|
||||
): [ComputedRef<Locale[T]>] {
|
||||
const localeData = inject<LocaleReceiverContext>('localeData', {} as LocaleReceiverContext);
|
||||
const componentLocale = computed(() => {
|
||||
const { antLocale } = localeData;
|
||||
const locale =
|
||||
defaultLocale || (defaultLocaleData as LocaleInterface)[componentName || 'global'];
|
||||
const localeFromContext = componentName && antLocale ? antLocale[componentName] : {};
|
||||
|
||||
return {
|
||||
...(typeof locale === 'function' ? (locale as Function)() : locale),
|
||||
...(localeFromContext || {}),
|
||||
};
|
||||
});
|
||||
return [componentLocale];
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { provide, App, defineComponent, VNode, PropType, reactive } from 'vue';
|
||||
import { provide, App, defineComponent, VNode, PropType, reactive, watch, onUnmounted } from 'vue';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import moment from 'moment';
|
||||
import interopDefault from '../_util/interopDefault';
|
||||
import { ModalLocale, changeConfirmLocale } from '../modal/locale';
|
||||
import warning from '../_util/warning';
|
||||
import { getSlot } from '../_util/props-util';
|
||||
import { withInstall } from '../_util/type';
|
||||
import { ValidateMessages } from '../form/interface';
|
||||
export interface Locale {
|
||||
locale: string;
|
||||
Pagination?: Object;
|
||||
|
@ -18,6 +18,14 @@ export interface Locale {
|
|||
Transfer?: Object;
|
||||
Select?: Object;
|
||||
Upload?: Object;
|
||||
|
||||
Form?: {
|
||||
optional?: string;
|
||||
defaultValidateMessages: ValidateMessages;
|
||||
};
|
||||
Image?: {
|
||||
preview: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LocaleProviderProps {
|
||||
|
@ -44,7 +52,7 @@ const LocaleProvider = defineComponent({
|
|||
},
|
||||
ANT_MARK__: PropTypes.string,
|
||||
},
|
||||
setup(props) {
|
||||
setup(props, { slots }) {
|
||||
warning(
|
||||
props.ANT_MARK__ === ANT_MARK,
|
||||
'LocaleProvider',
|
||||
|
@ -58,28 +66,25 @@ const LocaleProvider = defineComponent({
|
|||
ANT_MARK__: ANT_MARK,
|
||||
});
|
||||
provide('localeData', state);
|
||||
return { state };
|
||||
},
|
||||
watch: {
|
||||
locale(val) {
|
||||
this.state.antLocale = {
|
||||
...val,
|
||||
exist: true,
|
||||
};
|
||||
setMomentLocale(val);
|
||||
changeConfirmLocale(val && val.Modal);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
const { locale } = this;
|
||||
setMomentLocale(locale);
|
||||
changeConfirmLocale(locale && locale.Modal);
|
||||
},
|
||||
beforeUnmount() {
|
||||
changeConfirmLocale();
|
||||
},
|
||||
render() {
|
||||
return getSlot(this);
|
||||
watch(
|
||||
() => props.locale,
|
||||
val => {
|
||||
state.antLocale = {
|
||||
...val,
|
||||
exist: true,
|
||||
};
|
||||
setMomentLocale(val);
|
||||
changeConfirmLocale(val && val.Modal);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
onUnmounted(() => {
|
||||
changeConfirmLocale();
|
||||
});
|
||||
|
||||
return () => {
|
||||
return slots.default?.();
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -3,14 +3,13 @@ import DatePicker from '../date-picker/locale/en_US';
|
|||
import TimePicker from '../time-picker/locale/en_US';
|
||||
import Calendar from '../calendar/locale/en_US';
|
||||
// import ColorPicker from '../color-picker/locale/en_US';
|
||||
|
||||
const typeTemplate = '${label} is not a valid ${type}';
|
||||
export default {
|
||||
locale: 'en',
|
||||
Pagination,
|
||||
DatePicker,
|
||||
TimePicker,
|
||||
Calendar,
|
||||
// ColorPicker,
|
||||
global: {
|
||||
placeholder: 'Please select',
|
||||
},
|
||||
|
@ -18,11 +17,18 @@ export default {
|
|||
filterTitle: 'Filter menu',
|
||||
filterConfirm: 'OK',
|
||||
filterReset: 'Reset',
|
||||
filterEmptyText: 'No filters',
|
||||
emptyText: 'No data',
|
||||
selectAll: 'Select current page',
|
||||
selectInvert: 'Invert current page',
|
||||
selectNone: 'Clear all data',
|
||||
selectionAll: 'Select all data',
|
||||
sortTitle: 'Sort',
|
||||
expand: 'Expand row',
|
||||
collapse: 'Collapse row',
|
||||
triggerDesc: 'Click to sort descending',
|
||||
triggerAsc: 'Click to sort ascending',
|
||||
cancelSort: 'Click to cancel sorting',
|
||||
},
|
||||
Modal: {
|
||||
okText: 'OK',
|
||||
|
@ -38,6 +44,12 @@ export default {
|
|||
searchPlaceholder: 'Search here',
|
||||
itemUnit: 'item',
|
||||
itemsUnit: 'items',
|
||||
remove: 'Remove',
|
||||
selectCurrent: 'Select current page',
|
||||
removeCurrent: 'Remove current page',
|
||||
selectAll: 'Select all data',
|
||||
removeAll: 'Remove all data',
|
||||
selectInvert: 'Invert current page',
|
||||
},
|
||||
Upload: {
|
||||
uploading: 'Uploading...',
|
||||
|
@ -61,4 +73,57 @@ export default {
|
|||
PageHeader: {
|
||||
back: 'Back',
|
||||
},
|
||||
Form: {
|
||||
optional: '(optional)',
|
||||
defaultValidateMessages: {
|
||||
default: 'Field validation error for ${label}',
|
||||
required: 'Please enter ${label}',
|
||||
enum: '${label} must be one of [${enum}]',
|
||||
whitespace: '${label} cannot be a blank character',
|
||||
date: {
|
||||
format: '${label} date format is invalid',
|
||||
parse: '${label} cannot be converted to a date',
|
||||
invalid: '${label} is an invalid date',
|
||||
},
|
||||
types: {
|
||||
string: typeTemplate,
|
||||
method: typeTemplate,
|
||||
array: typeTemplate,
|
||||
object: typeTemplate,
|
||||
number: typeTemplate,
|
||||
date: typeTemplate,
|
||||
boolean: typeTemplate,
|
||||
integer: typeTemplate,
|
||||
float: typeTemplate,
|
||||
regexp: typeTemplate,
|
||||
email: typeTemplate,
|
||||
url: typeTemplate,
|
||||
hex: typeTemplate,
|
||||
},
|
||||
string: {
|
||||
len: '${label} must be ${len} characters',
|
||||
min: '${label} must be at least ${min} characters',
|
||||
max: '${label} must be up to ${max} characters',
|
||||
range: '${label} must be between ${min}-${max} characters',
|
||||
},
|
||||
number: {
|
||||
len: '${label} must be equal to ${len}',
|
||||
min: '${label} must be minimum ${min}',
|
||||
max: '${label} must be maximum ${max}',
|
||||
range: '${label} must be between ${min}-${max}',
|
||||
},
|
||||
array: {
|
||||
len: 'Must be ${len} ${label}',
|
||||
min: 'At least ${min} ${label}',
|
||||
max: 'At most ${max} ${label}',
|
||||
range: 'The amount of ${label} must be between ${min}-${max}',
|
||||
},
|
||||
pattern: {
|
||||
mismatch: '${label} does not match the pattern ${pattern}',
|
||||
},
|
||||
},
|
||||
},
|
||||
Image: {
|
||||
preview: 'Preview',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
import { defineComponent, inject } from 'vue';
|
||||
import { Item, itemProps } from '../vc-menu';
|
||||
import { getOptionProps, getSlot } from '../_util/props-util';
|
||||
import Tooltip, { TooltipProps } from '../tooltip';
|
||||
import { SiderContextProps } from '../layout/Sider';
|
||||
import { injectExtraPropsKey } from '../vc-menu/FunctionProvider';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MenuItem',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
...itemProps,
|
||||
onClick: PropTypes.func,
|
||||
},
|
||||
isMenuItem: true,
|
||||
setup() {
|
||||
return {
|
||||
getInlineCollapsed: inject<() => boolean>('getInlineCollapsed', () => false),
|
||||
layoutSiderContext: inject<SiderContextProps>('layoutSiderContext', {}),
|
||||
injectExtraProps: inject(injectExtraPropsKey, () => ({})),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onKeyDown(e: HTMLElement) {
|
||||
(this.$refs.menuItem as any).onKeyDown(e);
|
||||
},
|
||||
},
|
||||
render() {
|
||||
const props = getOptionProps(this);
|
||||
const { level, title, rootPrefixCls } = { ...props, ...this.injectExtraProps } as any;
|
||||
const { getInlineCollapsed, $attrs: attrs } = this;
|
||||
const inlineCollapsed = getInlineCollapsed();
|
||||
let tooltipTitle = title;
|
||||
const children = getSlot(this);
|
||||
if (typeof title === 'undefined') {
|
||||
tooltipTitle = level === 1 ? children : '';
|
||||
} else if (title === false) {
|
||||
tooltipTitle = '';
|
||||
}
|
||||
const tooltipProps: TooltipProps = {
|
||||
title: tooltipTitle,
|
||||
};
|
||||
const siderCollapsed = this.layoutSiderContext.sCollapsed;
|
||||
if (!siderCollapsed && !inlineCollapsed) {
|
||||
tooltipProps.title = null;
|
||||
// Reset `visible` to fix control mode tooltip display not correct
|
||||
// ref: https://github.com/ant-design/ant-design/issues/16742
|
||||
tooltipProps.visible = false;
|
||||
}
|
||||
|
||||
const itemProps = {
|
||||
...props,
|
||||
title,
|
||||
...attrs,
|
||||
ref: 'menuItem',
|
||||
};
|
||||
const toolTipProps: TooltipProps = {
|
||||
...tooltipProps,
|
||||
placement: 'right',
|
||||
overlayClassName: `${rootPrefixCls}-inline-collapsed-tooltip`,
|
||||
};
|
||||
const item = <Item {...itemProps}>{children}</Item>;
|
||||
return <Tooltip {...toolTipProps}>{item}</Tooltip>;
|
||||
},
|
||||
});
|
|
@ -1,42 +0,0 @@
|
|||
import { defineComponent, inject } from 'vue';
|
||||
import { SubMenu as VcSubMenu } from '../vc-menu';
|
||||
import classNames from '../_util/classNames';
|
||||
import { injectExtraPropsKey } from '../vc-menu/FunctionProvider';
|
||||
|
||||
export type MenuTheme = 'light' | 'dark';
|
||||
|
||||
export interface MenuContextProps {
|
||||
inlineCollapsed?: boolean;
|
||||
theme?: MenuTheme;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ASubMenu',
|
||||
isSubMenu: true,
|
||||
inheritAttrs: false,
|
||||
props: { ...VcSubMenu.props },
|
||||
setup() {
|
||||
return {
|
||||
menuPropsContext: inject<MenuContextProps>('menuPropsContext', {}),
|
||||
injectExtraProps: inject(injectExtraPropsKey, () => ({})),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onKeyDown(e: Event) {
|
||||
(this.$refs.subMenu as any).onKeyDown(e);
|
||||
},
|
||||
},
|
||||
|
||||
render() {
|
||||
const { $slots, $attrs } = this;
|
||||
const { rootPrefixCls, popupClassName } = { ...this.$props, ...this.injectExtraProps } as any;
|
||||
const { theme: antdMenuTheme } = this.menuPropsContext;
|
||||
const props = {
|
||||
...this.$props,
|
||||
popupClassName: classNames(`${rootPrefixCls}-${antdMenuTheme}`, popupClassName),
|
||||
ref: 'subMenu',
|
||||
...$attrs,
|
||||
} as any;
|
||||
return <VcSubMenu {...props} v-slots={$slots}></VcSubMenu>;
|
||||
},
|
||||
});
|
|
@ -1,327 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders ./antdv-demo/docs/menu/demo/horizontal.md correctly 1`] = `
|
||||
<div>
|
||||
<ul role="menu" class="ant-menu-light ant-menu-root ant-menu ant-menu-horizontal">
|
||||
<li class="ant-menu-submenu ant-menu-submenu-horizontal ant-menu-overflowed-submenu" style="display: none;" role="menuitem">
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<div aria-expanded="false" aria-haspopup="true" class="ant-menu-submenu-title"><span>···</span><i class="ant-menu-submenu-arrow"></i></div>
|
||||
</li>
|
||||
<!---->
|
||||
<li role="menuitem" class="ant-menu-item ant-menu-item-selected"><span role="img" aria-label="mail" class="anticon anticon-mail"><svg class="" data-icon="mail" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5zM833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 341.6 265.6a55.99 55.99 0 0068.7 0L888 270.8l27.6-21.5-39.3-50.5-42.7 33.2z"></path></svg></span>Navigation One
|
||||
<!---->
|
||||
</li>
|
||||
<li class="ant-menu-submenu ant-menu-submenu-horizontal ant-menu-overflowed-submenu" style="display: none;" role="menuitem">
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<div aria-expanded="false" aria-haspopup="true" class="ant-menu-submenu-title"><span>···</span><i class="ant-menu-submenu-arrow"></i></div>
|
||||
</li>
|
||||
<!---->
|
||||
<li role="menuitem" aria-disabled="true" class="ant-menu-item ant-menu-item-disabled"><span role="img" aria-label="appstore" class="anticon anticon-appstore"><svg class="" data-icon="appstore" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H212V212h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H612V212h200v200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H212V612h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H612V612h200v200z"></path></svg></span>Navigation Two
|
||||
<!---->
|
||||
</li>
|
||||
<li class="ant-menu-submenu ant-menu-submenu-horizontal ant-menu-overflowed-submenu" style="display: none;" role="menuitem">
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<div aria-expanded="false" aria-haspopup="true" class="ant-menu-submenu-title"><span>···</span><i class="ant-menu-submenu-arrow"></i></div>
|
||||
</li>
|
||||
<li class="ant-menu-submenu ant-menu-submenu-horizontal" role="menuitem">
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<div aria-expanded="false" aria-haspopup="true" class="ant-menu-submenu-title"><span class="submenu-title-wrapper"><span role="img" aria-label="setting" class="anticon anticon-setting"><svg class="" data-icon="setting" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z"></path></svg></span> Navigation Three - Submenu </span><i class="ant-menu-submenu-arrow"></i></div>
|
||||
</li>
|
||||
<li class="ant-menu-submenu ant-menu-submenu-horizontal ant-menu-overflowed-submenu" style="display: none;" role="menuitem">
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<div aria-expanded="false" aria-haspopup="true" class="ant-menu-submenu-title"><span>···</span><i class="ant-menu-submenu-arrow"></i></div>
|
||||
</li>
|
||||
<!---->
|
||||
<li role="menuitem" class="ant-menu-item"><a href="https://antdv.com" target="_blank" rel="noopener noreferrer"> Navigation Four - Link </a>
|
||||
<!---->
|
||||
</li>
|
||||
<li class="ant-menu-submenu ant-menu-submenu-horizontal ant-menu-overflowed-submenu" style="visibility: hidden; position: absolute; display: none;" role="menuitem">
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<div aria-expanded="false" aria-haspopup="true" class="ant-menu-submenu-title"><span>···</span><i class="ant-menu-submenu-arrow"></i></div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./antdv-demo/docs/menu/demo/inline.md correctly 1`] = `
|
||||
<div>
|
||||
<ul role="menu" class="ant-menu-light ant-menu-root ant-menu ant-menu-inline" style="width: 256px;" id="dddddd">
|
||||
<li class="ant-menu-submenu ant-menu-submenu-inline ant-menu-submenu-open ant-menu-submenu-selected" role="menuitem">
|
||||
<div aria-expanded="true" aria-owns="sub1$Menu" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="mail" class="anticon anticon-mail"><svg class="" data-icon="mail" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5zM833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 341.6 265.6a55.99 55.99 0 0068.7 0L888 270.8l27.6-21.5-39.3-50.5-42.7 33.2z"></path></svg></span><span>Navigation One</span></span><i class="ant-menu-submenu-arrow"></i></div>
|
||||
<ul role="menu" class="ant-menu-sub ant-menu ant-menu-inline" id="sub1$Menu">
|
||||
<li class="ant-menu-item-group">
|
||||
<div class="ant-menu-item-group-title"><span role="img" aria-label="qq" class="anticon anticon-qq"><svg class="" data-icon="qq" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M824.8 613.2c-16-51.4-34.4-94.6-62.7-165.3C766.5 262.2 689.3 112 511.5 112 331.7 112 256.2 265.2 261 447.9c-28.4 70.8-46.7 113.7-62.7 165.3-34 109.5-23 154.8-14.6 155.8 18 2.2 70.1-82.4 70.1-82.4 0 49 25.2 112.9 79.8 159-26.4 8.1-85.7 29.9-71.6 53.8 11.4 19.3 196.2 12.3 249.5 6.3 53.3 6 238.1 13 249.5-6.3 14.1-23.8-45.3-45.7-71.6-53.8 54.6-46.2 79.8-110.1 79.8-159 0 0 52.1 84.6 70.1 82.4 8.5-1.1 19.5-46.4-14.5-155.8z"></path></svg></span><span>Item 1</span></div>
|
||||
<ul class="ant-menu-item-group-list">
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item ant-menu-item-selected">Option 1
|
||||
<!---->
|
||||
</li>
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 2
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="ant-menu-item-group">
|
||||
<div class="ant-menu-item-group-title" title="Item 2">Item 2</div>
|
||||
<ul class="ant-menu-item-group-list">
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item"> Option 3
|
||||
<!---->
|
||||
</li>
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item"> Option 4
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<!---->
|
||||
</li>
|
||||
<li class="ant-menu-submenu ant-menu-submenu-inline" role="menuitem">
|
||||
<div aria-expanded="false" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="appstore" class="anticon anticon-appstore"><svg class="" data-icon="appstore" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H212V212h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H612V212h200v200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H212V612h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H612V612h200v200z"></path></svg></span><span>Navigation Two</span></span><i class="ant-menu-submenu-arrow"></i></div>
|
||||
<div></div>
|
||||
<!---->
|
||||
</li>
|
||||
<li class="ant-menu-submenu ant-menu-submenu-inline" role="menuitem">
|
||||
<div aria-expanded="false" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="setting" class="anticon anticon-setting"><svg class="" data-icon="setting" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z"></path></svg></span><span>Navigation Three</span></span><i class="ant-menu-submenu-arrow"></i></div>
|
||||
<div></div>
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./antdv-demo/docs/menu/demo/inline-collapsed.md correctly 1`] = `
|
||||
<div style="width: 256px;"><button style="margin-bottom: 16px;" class="ant-btn ant-btn-primary" type="button">
|
||||
<!----><span role="img" aria-label="menu-fold" class="anticon anticon-menu-fold"><svg class="" data-icon="menu-fold" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1a8.74 8.74 0 000 13.8z"></path></svg></span>
|
||||
</button>
|
||||
<ul role="menu" class="ant-menu-dark ant-menu-root ant-menu ant-menu-inline">
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 24px;" class="ant-menu-item ant-menu-item-selected"><span role="img" aria-label="pie-chart" class="anticon anticon-pie-chart"><svg class="" data-icon="pie-chart" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M864 518H506V160c0-4.4-3.6-8-8-8h-26a398.46 398.46 0 00-282.8 117.1 398.19 398.19 0 00-85.7 127.1A397.61 397.61 0 0072 552a398.46 398.46 0 00117.1 282.8c36.7 36.7 79.5 65.6 127.1 85.7A397.61 397.61 0 00472 952a398.46 398.46 0 00282.8-117.1c36.7-36.7 65.6-79.5 85.7-127.1A397.61 397.61 0 00872 552v-26c0-4.4-3.6-8-8-8zM705.7 787.8A331.59 331.59 0 01470.4 884c-88.1-.4-170.9-34.9-233.2-97.2C174.5 724.1 140 640.7 140 552c0-88.7 34.5-172.1 97.2-234.8 54.6-54.6 124.9-87.9 200.8-95.5V586h364.3c-7.7 76.3-41.3 147-96.6 201.8zM952 462.4l-2.6-28.2c-8.5-92.1-49.4-179-115.2-244.6A399.4 399.4 0 00589 74.6L560.7 72c-4.7-.4-8.7 3.2-8.7 7.9V464c0 4.4 3.6 8 8 8l384-1c4.7 0 8.4-4 8-8.6zm-332.2-58.2V147.6a332.24 332.24 0 01166.4 89.8c45.7 45.6 77 103.6 90 166.1l-256.4.7z"></path></svg></span><span>Option 1</span>
|
||||
<!---->
|
||||
</li>
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 24px;" class="ant-menu-item"><span role="img" aria-label="desktop" class="anticon anticon-desktop"><svg class="" data-icon="desktop" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M928 140H96c-17.7 0-32 14.3-32 32v496c0 17.7 14.3 32 32 32h380v112H304c-8.8 0-16 7.2-16 16v48c0 4.4 3.6 8 8 8h432c4.4 0 8-3.6 8-8v-48c0-8.8-7.2-16-16-16H548V700h380c17.7 0 32-14.3 32-32V172c0-17.7-14.3-32-32-32zm-40 488H136V212h752v416z"></path></svg></span><span>Option 2</span>
|
||||
<!---->
|
||||
</li>
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 24px;" class="ant-menu-item"><span role="img" aria-label="inbox" class="anticon anticon-inbox"><svg class="" data-icon="inbox" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="0 0 1024 1024" focusable="false"><path d="M885.2 446.3l-.2-.8-112.2-285.1c-5-16.1-19.9-27.2-36.8-27.2H281.2c-17 0-32.1 11.3-36.9 27.6L139.4 443l-.3.7-.2.8c-1.3 4.9-1.7 9.9-1 14.8-.1 1.6-.2 3.2-.2 4.8V830a60.9 60.9 0 0060.8 60.8h627.2c33.5 0 60.8-27.3 60.9-60.8V464.1c0-1.3 0-2.6-.1-3.7.4-4.9 0-9.6-1.3-14.1zm-295.8-43l-.3 15.7c-.8 44.9-31.8 75.1-77.1 75.1-22.1 0-41.1-7.1-54.8-20.6S436 441.2 435.6 419l-.3-15.7H229.5L309 210h399.2l81.7 193.3H589.4zm-375 76.8h157.3c24.3 57.1 76 90.8 140.4 90.8 33.7 0 65-9.4 90.3-27.2 22.2-15.6 39.5-37.4 50.7-63.6h156.5V814H214.4V480.1z"></path></svg></span><span>Option 3</span>
|
||||
<!---->
|
||||
</li>
|
||||
<li class="ant-menu-submenu ant-menu-submenu-inline ant-menu-submenu-open" role="menuitem">
|
||||
<div aria-expanded="true" aria-owns="sub1$Menu" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="mail" class="anticon anticon-mail"><svg class="" data-icon="mail" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5zM833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 341.6 265.6a55.99 55.99 0 0068.7 0L888 270.8l27.6-21.5-39.3-50.5-42.7 33.2z"></path></svg></span><span>Navigation One</span></span><i class="ant-menu-submenu-arrow"></i></div>
|
||||
<ul role="menu" class="ant-menu-sub ant-menu ant-menu-inline" id="sub1$Menu">
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 5
|
||||
<!---->
|
||||
</li>
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 6
|
||||
<!---->
|
||||
</li>
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 7
|
||||
<!---->
|
||||
</li>
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 8
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
<!---->
|
||||
</li>
|
||||
<li class="ant-menu-submenu ant-menu-submenu-inline" role="menuitem">
|
||||
<div aria-expanded="false" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="appstore" class="anticon anticon-appstore"><svg class="" data-icon="appstore" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H212V212h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H612V212h200v200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H212V612h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H612V612h200v200z"></path></svg></span><span>Navigation Two</span></span><i class="ant-menu-submenu-arrow"></i></div>
|
||||
<div></div>
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./antdv-demo/docs/menu/demo/sider-current.md correctly 1`] = `
|
||||
<div>
|
||||
<ul role="menu" class="ant-menu-light ant-menu-root ant-menu ant-menu-inline" style="width: 256px;">
|
||||
<li class="ant-menu-submenu ant-menu-submenu-inline ant-menu-submenu-open" role="menuitem">
|
||||
<div aria-expanded="true" aria-owns="sub1$Menu" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="mail" class="anticon anticon-mail"><svg class="" data-icon="mail" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5zM833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 341.6 265.6a55.99 55.99 0 0068.7 0L888 270.8l27.6-21.5-39.3-50.5-42.7 33.2z"></path></svg></span><span>Navigation One</span></span><i class="ant-menu-submenu-arrow"></i></div>
|
||||
<ul role="menu" class="ant-menu-sub ant-menu ant-menu-inline" id="sub1$Menu">
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 1
|
||||
<!---->
|
||||
</li>
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 2
|
||||
<!---->
|
||||
</li>
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 3
|
||||
<!---->
|
||||
</li>
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 4
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
<!---->
|
||||
</li>
|
||||
<li class="ant-menu-submenu ant-menu-submenu-inline" role="menuitem">
|
||||
<div aria-expanded="false" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="appstore" class="anticon anticon-appstore"><svg class="" data-icon="appstore" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H212V212h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H612V212h200v200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H212V612h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H612V612h200v200z"></path></svg></span><span>Navigation Two</span></span><i class="ant-menu-submenu-arrow"></i></div>
|
||||
<div></div>
|
||||
<!---->
|
||||
</li>
|
||||
<li class="ant-menu-submenu ant-menu-submenu-inline" role="menuitem">
|
||||
<div aria-expanded="false" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="setting" class="anticon anticon-setting"><svg class="" data-icon="setting" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z"></path></svg></span><span>Navigation Three</span></span><i class="ant-menu-submenu-arrow"></i></div>
|
||||
<div></div>
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./antdv-demo/docs/menu/demo/switch-mode.md correctly 1`] = `
|
||||
<div><button class="ant-switch" type="button" role="switch" aria-checked="false">
|
||||
<!----><span class="ant-switch-inner"><!----></span>
|
||||
</button> Change Mode <span class="ant-divider" style="margin: 0px 1em;"></span><button class="ant-switch" type="button" role="switch" aria-checked="false">
|
||||
<!----><span class="ant-switch-inner"><!----></span>
|
||||
</button> Change Theme <br><br>
|
||||
<ul role="menu" class="ant-menu-light ant-menu-root ant-menu ant-menu-inline" style="width: 256px;">
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 24px;" class="ant-menu-item ant-menu-item-selected"><span role="img" aria-label="mail" class="anticon anticon-mail"><svg class="" data-icon="mail" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5zM833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 341.6 265.6a55.99 55.99 0 0068.7 0L888 270.8l27.6-21.5-39.3-50.5-42.7 33.2z"></path></svg></span> Navigation One
|
||||
<!---->
|
||||
</li>
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 24px;" class="ant-menu-item"><span role="img" aria-label="calendar" class="anticon anticon-calendar"><svg class="" data-icon="calendar" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M880 184H712v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H384v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H144c-17.7 0-32 14.3-32 32v664c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V216c0-17.7-14.3-32-32-32zm-40 656H184V460h656v380zM184 392V256h128v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h256v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h128v136H184z"></path></svg></span> Navigation Two
|
||||
<!---->
|
||||
</li>
|
||||
<li class="ant-menu-submenu ant-menu-submenu-inline ant-menu-submenu-open" role="menuitem">
|
||||
<div aria-expanded="true" aria-owns="sub1$Menu" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="appstore" class="anticon anticon-appstore"><svg class="" data-icon="appstore" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H212V212h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H612V212h200v200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H212V612h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H612V612h200v200z"></path></svg></span><span>Navigation Three</span></span><i class="ant-menu-submenu-arrow"></i></div>
|
||||
<ul role="menu" class="ant-menu-sub ant-menu ant-menu-inline" id="sub1$Menu">
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 3
|
||||
<!---->
|
||||
</li>
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 4
|
||||
<!---->
|
||||
</li>
|
||||
<li class="ant-menu-submenu ant-menu-submenu-inline" role="menuitem">
|
||||
<div aria-expanded="false" aria-haspopup="true" title="Submenu" style="padding-left: 48px;" class="ant-menu-submenu-title">Submenu<i class="ant-menu-submenu-arrow"></i></div>
|
||||
<div></div>
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
<!---->
|
||||
</li>
|
||||
<li class="ant-menu-submenu ant-menu-submenu-inline" role="menuitem">
|
||||
<div aria-expanded="false" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="setting" class="anticon anticon-setting"><svg class="" data-icon="setting" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z"></path></svg></span><span>Navigation Four</span></span><i class="ant-menu-submenu-arrow"></i></div>
|
||||
<div></div>
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./antdv-demo/docs/menu/demo/template.md correctly 1`] = `
|
||||
<div style="width: 256px;"><button style="margin-bottom: 16px;" class="ant-btn ant-btn-primary" type="button">
|
||||
<!----><span role="img" aria-label="menu-fold" class="anticon anticon-menu-fold"><svg class="" data-icon="menu-fold" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1a8.74 8.74 0 000 13.8z"></path></svg></span>
|
||||
</button>
|
||||
<ul role="menu" class="ant-menu-dark ant-menu-root ant-menu ant-menu-inline">
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 24px;" class="ant-menu-item ant-menu-item-selected"><span role="img" aria-label="pie-chart" class="anticon anticon-pie-chart"><svg class="" data-icon="pie-chart" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M864 518H506V160c0-4.4-3.6-8-8-8h-26a398.46 398.46 0 00-282.8 117.1 398.19 398.19 0 00-85.7 127.1A397.61 397.61 0 0072 552a398.46 398.46 0 00117.1 282.8c36.7 36.7 79.5 65.6 127.1 85.7A397.61 397.61 0 00472 952a398.46 398.46 0 00282.8-117.1c36.7-36.7 65.6-79.5 85.7-127.1A397.61 397.61 0 00872 552v-26c0-4.4-3.6-8-8-8zM705.7 787.8A331.59 331.59 0 01470.4 884c-88.1-.4-170.9-34.9-233.2-97.2C174.5 724.1 140 640.7 140 552c0-88.7 34.5-172.1 97.2-234.8 54.6-54.6 124.9-87.9 200.8-95.5V586h364.3c-7.7 76.3-41.3 147-96.6 201.8zM952 462.4l-2.6-28.2c-8.5-92.1-49.4-179-115.2-244.6A399.4 399.4 0 00589 74.6L560.7 72c-4.7-.4-8.7 3.2-8.7 7.9V464c0 4.4 3.6 8 8 8l384-1c4.7 0 8.4-4 8-8.6zm-332.2-58.2V147.6a332.24 332.24 0 01166.4 89.8c45.7 45.6 77 103.6 90 166.1l-256.4.7z"></path></svg></span><span>Option 1</span>
|
||||
<!---->
|
||||
</li>
|
||||
<li class="ant-menu-submenu ant-menu-submenu-inline ant-menu-submenu-open" role="menuitem">
|
||||
<div aria-expanded="true" aria-owns="2$Menu" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="mail" class="anticon anticon-mail"><svg class="" data-icon="mail" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5zM833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 341.6 265.6a55.99 55.99 0 0068.7 0L888 270.8l27.6-21.5-39.3-50.5-42.7 33.2z"></path></svg></span><span>Navigation 2</span></span><i class="ant-menu-submenu-arrow"></i></div>
|
||||
<ul role="menu" class="ant-menu-sub ant-menu ant-menu-inline" id="2$Menu">
|
||||
<li class="ant-menu-submenu ant-menu-submenu-inline" role="menuitem">
|
||||
<div aria-expanded="false" aria-haspopup="true" style="padding-left: 48px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="mail" class="anticon anticon-mail"><svg class="" data-icon="mail" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5zM833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 341.6 265.6a55.99 55.99 0 0068.7 0L888 270.8l27.6-21.5-39.3-50.5-42.7 33.2z"></path></svg></span><span>Navigation 3</span></span><i class="ant-menu-submenu-arrow"></i></div>
|
||||
<div></div>
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./antdv-demo/docs/menu/demo/theme.md correctly 1`] = `
|
||||
<div><button class="ant-switch ant-switch-checked" type="button" role="switch" aria-checked="true">
|
||||
<!----><span class="ant-switch-inner">dark</span>
|
||||
</button><br><br>
|
||||
<ul role="menu" class="ant-menu-dark ant-menu-root ant-menu ant-menu-inline" style="width: 256px;">
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 24px;" class="ant-menu-item ant-menu-item-selected"><span role="img" aria-label="mail" class="anticon anticon-mail"><svg class="" data-icon="mail" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5zM833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 341.6 265.6a55.99 55.99 0 0068.7 0L888 270.8l27.6-21.5-39.3-50.5-42.7 33.2z"></path></svg></span> Navigation One
|
||||
<!---->
|
||||
</li>
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 24px;" class="ant-menu-item"><span role="img" aria-label="calendar" class="anticon anticon-calendar"><svg class="" data-icon="calendar" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M880 184H712v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H384v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H144c-17.7 0-32 14.3-32 32v664c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V216c0-17.7-14.3-32-32-32zm-40 656H184V460h656v380zM184 392V256h128v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h256v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h128v136H184z"></path></svg></span> Navigation Two
|
||||
<!---->
|
||||
</li>
|
||||
<li class="ant-menu-submenu ant-menu-submenu-inline ant-menu-submenu-open" role="menuitem">
|
||||
<div aria-expanded="true" aria-owns="sub1$Menu" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="appstore" class="anticon anticon-appstore"><svg class="" data-icon="appstore" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H212V212h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H612V212h200v200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H212V612h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H612V612h200v200z"></path></svg></span><span>Navigation Three</span></span><i class="ant-menu-submenu-arrow"></i></div>
|
||||
<ul role="menu" class="ant-menu-sub ant-menu ant-menu-inline" id="sub1$Menu">
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 3
|
||||
<!---->
|
||||
</li>
|
||||
<!---->
|
||||
<li role="menuitem" style="padding-left: 48px;" class="ant-menu-item">Option 4
|
||||
<!---->
|
||||
</li>
|
||||
<li class="ant-menu-submenu ant-menu-submenu-inline" role="menuitem">
|
||||
<div aria-expanded="false" aria-haspopup="true" title="Submenu" style="padding-left: 48px;" class="ant-menu-submenu-title">Submenu<i class="ant-menu-submenu-arrow"></i></div>
|
||||
<div></div>
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
<!---->
|
||||
</li>
|
||||
<li class="ant-menu-submenu ant-menu-submenu-inline" role="menuitem">
|
||||
<div aria-expanded="false" aria-haspopup="true" style="padding-left: 24px;" class="ant-menu-submenu-title"><span><span role="img" aria-label="setting" class="anticon anticon-setting"><svg class="" data-icon="setting" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z"></path></svg></span><span>Navigation Four</span></span><i class="ant-menu-submenu-arrow"></i></div>
|
||||
<div></div>
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./antdv-demo/docs/menu/demo/vertical.md correctly 1`] = `
|
||||
<div>
|
||||
<ul role="menu" class="ant-menu-light ant-menu-root ant-menu ant-menu-vertical" style="width: 256px;">
|
||||
<!---->
|
||||
<li role="menuitem" class="ant-menu-item"><span role="img" aria-label="mail" class="anticon anticon-mail"><svg class="" data-icon="mail" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5zM833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 341.6 265.6a55.99 55.99 0 0068.7 0L888 270.8l27.6-21.5-39.3-50.5-42.7 33.2z"></path></svg></span> Navigation One
|
||||
<!---->
|
||||
</li>
|
||||
<!---->
|
||||
<li role="menuitem" class="ant-menu-item"><span role="img" aria-label="calendar" class="anticon anticon-calendar"><svg class="" data-icon="calendar" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M880 184H712v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H384v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H144c-17.7 0-32 14.3-32 32v664c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V216c0-17.7-14.3-32-32-32zm-40 656H184V460h656v380zM184 392V256h128v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h256v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h128v136H184z"></path></svg></span> Navigation Two
|
||||
<!---->
|
||||
</li>
|
||||
<li class="ant-menu-submenu ant-menu-submenu-vertical" role="menuitem">
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<div aria-expanded="false" aria-haspopup="true" class="ant-menu-submenu-title"><span><span role="img" aria-label="appstore" class="anticon anticon-appstore"><svg class="" data-icon="appstore" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H212V212h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H612V212h200v200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H212V612h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H612V612h200v200z"></path></svg></span><span>Navigation Three</span></span><i class="ant-menu-submenu-arrow"></i></div>
|
||||
</li>
|
||||
<li class="ant-menu-submenu ant-menu-submenu-vertical" role="menuitem">
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<div aria-expanded="false" aria-haspopup="true" class="ant-menu-submenu-title"><span><span role="img" aria-label="setting" class="anticon anticon-setting"><svg class="" data-icon="setting" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896" focusable="false"><path d="M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z"></path></svg></span><span>Navigation Four</span></span><i class="ant-menu-submenu-arrow"></i></div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
|
@ -1,322 +1,39 @@
|
|||
import { defineComponent, inject, provide, toRef, App, ExtractPropTypes, Plugin } from 'vue';
|
||||
import omit from 'omit.js';
|
||||
import VcMenu, { Divider, ItemGroup } from '../vc-menu';
|
||||
import SubMenu from './SubMenu';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import animation from '../_util/openAnimation';
|
||||
import warning from '../_util/warning';
|
||||
import Item from './MenuItem';
|
||||
import { hasProp, getOptionProps } from '../_util/props-util';
|
||||
import BaseMixin from '../_util/BaseMixin';
|
||||
import commonPropsType from '../vc-menu/commonPropsType';
|
||||
import { defaultConfigProvider } from '../config-provider';
|
||||
import { SiderContextProps } from '../layout/Sider';
|
||||
import { tuple } from '../_util/type';
|
||||
// import raf from '../_util/raf';
|
||||
|
||||
export const MenuMode = PropTypes.oneOf([
|
||||
'vertical',
|
||||
'vertical-left',
|
||||
'vertical-right',
|
||||
'horizontal',
|
||||
'inline',
|
||||
]);
|
||||
|
||||
export const menuProps = {
|
||||
...commonPropsType,
|
||||
theme: PropTypes.oneOf(tuple('light', 'dark')).def('light'),
|
||||
mode: MenuMode.def('vertical'),
|
||||
selectable: PropTypes.looseBool,
|
||||
selectedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
|
||||
defaultSelectedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
|
||||
openKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
|
||||
defaultOpenKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
|
||||
openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
openTransitionName: PropTypes.string,
|
||||
prefixCls: PropTypes.string,
|
||||
multiple: PropTypes.looseBool,
|
||||
inlineIndent: PropTypes.number.def(24),
|
||||
inlineCollapsed: PropTypes.looseBool,
|
||||
isRootMenu: PropTypes.looseBool.def(true),
|
||||
focusable: PropTypes.looseBool.def(false),
|
||||
onOpenChange: PropTypes.func,
|
||||
onSelect: PropTypes.func,
|
||||
onDeselect: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
onMouseenter: PropTypes.func,
|
||||
onSelectChange: PropTypes.func,
|
||||
};
|
||||
|
||||
export type MenuProps = Partial<ExtractPropTypes<typeof menuProps>>;
|
||||
|
||||
const Menu = defineComponent({
|
||||
name: 'AMenu',
|
||||
mixins: [BaseMixin],
|
||||
inheritAttrs: false,
|
||||
props: menuProps,
|
||||
Divider: { ...Divider, name: 'AMenuDivider' },
|
||||
Item: { ...Item, name: 'AMenuItem' },
|
||||
SubMenu: { ...SubMenu, name: 'ASubMenu' },
|
||||
ItemGroup: { ...ItemGroup, name: 'AMenuItemGroup' },
|
||||
emits: [
|
||||
'update:selectedKeys',
|
||||
'update:openKeys',
|
||||
'mouseenter',
|
||||
'openChange',
|
||||
'click',
|
||||
'selectChange',
|
||||
'select',
|
||||
'deselect',
|
||||
],
|
||||
setup() {
|
||||
const layoutSiderContext = inject<SiderContextProps>('layoutSiderContext', {});
|
||||
const layoutSiderCollapsed = toRef(layoutSiderContext, 'sCollapsed');
|
||||
return {
|
||||
configProvider: inject('configProvider', defaultConfigProvider),
|
||||
layoutSiderContext,
|
||||
layoutSiderCollapsed,
|
||||
propsUpdating: false,
|
||||
switchingModeFromInline: false,
|
||||
leaveAnimationExecutedWhenInlineCollapsed: false,
|
||||
inlineOpenKeys: [],
|
||||
};
|
||||
},
|
||||
data() {
|
||||
const props: MenuProps = getOptionProps(this);
|
||||
warning(
|
||||
!('inlineCollapsed' in props && props.mode !== 'inline'),
|
||||
'Menu',
|
||||
"`inlineCollapsed` should only be used when Menu's `mode` is inline.",
|
||||
);
|
||||
let sOpenKeys: (number | string)[];
|
||||
|
||||
if ('openKeys' in props) {
|
||||
sOpenKeys = props.openKeys;
|
||||
} else if ('defaultOpenKeys' in props) {
|
||||
sOpenKeys = props.defaultOpenKeys;
|
||||
}
|
||||
return {
|
||||
sOpenKeys,
|
||||
};
|
||||
},
|
||||
// beforeUnmount() {
|
||||
// raf.cancel(this.mountRafId);
|
||||
// },
|
||||
watch: {
|
||||
mode(val, oldVal) {
|
||||
if (oldVal === 'inline' && val !== 'inline') {
|
||||
this.switchingModeFromInline = true;
|
||||
}
|
||||
},
|
||||
openKeys(val) {
|
||||
this.setState({ sOpenKeys: val });
|
||||
},
|
||||
inlineCollapsed(val) {
|
||||
this.collapsedChange(val);
|
||||
},
|
||||
layoutSiderCollapsed(val) {
|
||||
this.collapsedChange(val);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
provide('getInlineCollapsed', this.getInlineCollapsed);
|
||||
provide('menuPropsContext', this.$props);
|
||||
},
|
||||
updated() {
|
||||
this.propsUpdating = false;
|
||||
},
|
||||
methods: {
|
||||
collapsedChange(val: unknown) {
|
||||
if (this.propsUpdating) {
|
||||
return;
|
||||
}
|
||||
this.propsUpdating = true;
|
||||
if (!hasProp(this, 'openKeys')) {
|
||||
if (val) {
|
||||
this.switchingModeFromInline = true;
|
||||
this.inlineOpenKeys = this.sOpenKeys;
|
||||
this.setState({ sOpenKeys: [] });
|
||||
} else {
|
||||
this.setState({ sOpenKeys: this.inlineOpenKeys });
|
||||
this.inlineOpenKeys = [];
|
||||
}
|
||||
} else if (val) {
|
||||
// 缩起时,openKeys置为空的动画会闪动,react可以通过是否传递openKeys避免闪动,vue不是很方便动态传递openKeys
|
||||
this.switchingModeFromInline = true;
|
||||
}
|
||||
},
|
||||
restoreModeVerticalFromInline() {
|
||||
if (this.switchingModeFromInline) {
|
||||
this.switchingModeFromInline = false;
|
||||
this.$forceUpdate();
|
||||
}
|
||||
},
|
||||
// Restore vertical mode when menu is collapsed responsively when mounted
|
||||
// https://github.com/ant-design/ant-design/issues/13104
|
||||
// TODO: not a perfect solution, looking a new way to avoid setting switchingModeFromInline in this situation
|
||||
handleMouseEnter(e: Event) {
|
||||
this.restoreModeVerticalFromInline();
|
||||
this.$emit('mouseenter', e);
|
||||
},
|
||||
handleTransitionEnd(e: TransitionEvent) {
|
||||
// when inlineCollapsed menu width animation finished
|
||||
// https://github.com/ant-design/ant-design/issues/12864
|
||||
const widthCollapsed = e.propertyName === 'width' && e.target === e.currentTarget;
|
||||
|
||||
// Fix SVGElement e.target.className.indexOf is not a function
|
||||
// https://github.com/ant-design/ant-design/issues/15699
|
||||
const { className } = e.target as SVGAnimationElement | HTMLElement;
|
||||
// SVGAnimatedString.animVal should be identical to SVGAnimatedString.baseVal, unless during an animation.
|
||||
const classNameValue =
|
||||
Object.prototype.toString.call(className) === '[object SVGAnimatedString]'
|
||||
? className.animVal
|
||||
: className;
|
||||
|
||||
// Fix for <Menu style={{ width: '100%' }} />, the width transition won't trigger when menu is collapsed
|
||||
// https://github.com/ant-design/ant-design-pro/issues/2783
|
||||
const iconScaled = e.propertyName === 'font-size' && classNameValue.indexOf('anticon') >= 0;
|
||||
|
||||
if (widthCollapsed || iconScaled) {
|
||||
this.restoreModeVerticalFromInline();
|
||||
}
|
||||
},
|
||||
handleClick(e: Event) {
|
||||
this.handleOpenChange([]);
|
||||
this.$emit('click', e);
|
||||
},
|
||||
handleSelect(info) {
|
||||
this.$emit('update:selectedKeys', info.selectedKeys);
|
||||
this.$emit('select', info);
|
||||
this.$emit('selectChange', info.selectedKeys);
|
||||
},
|
||||
handleDeselect(info) {
|
||||
this.$emit('update:selectedKeys', info.selectedKeys);
|
||||
this.$emit('deselect', info);
|
||||
this.$emit('selectChange', info.selectedKeys);
|
||||
},
|
||||
handleOpenChange(openKeys: (number | string)[]) {
|
||||
this.setOpenKeys(openKeys);
|
||||
this.$emit('update:openKeys', openKeys);
|
||||
this.$emit('openChange', openKeys);
|
||||
},
|
||||
setOpenKeys(openKeys: (number | string)[]) {
|
||||
if (!hasProp(this, 'openKeys')) {
|
||||
this.setState({ sOpenKeys: openKeys });
|
||||
}
|
||||
},
|
||||
getRealMenuMode() {
|
||||
const inlineCollapsed = this.getInlineCollapsed();
|
||||
if (this.switchingModeFromInline && inlineCollapsed) {
|
||||
return 'inline';
|
||||
}
|
||||
const { mode } = this.$props;
|
||||
return inlineCollapsed ? 'vertical' : mode;
|
||||
},
|
||||
getInlineCollapsed() {
|
||||
const { inlineCollapsed } = this.$props;
|
||||
if (this.layoutSiderContext.sCollapsed !== undefined) {
|
||||
return this.layoutSiderContext.sCollapsed;
|
||||
}
|
||||
return inlineCollapsed;
|
||||
},
|
||||
getMenuOpenAnimation(menuMode: string) {
|
||||
const { openAnimation, openTransitionName } = this.$props;
|
||||
let menuOpenAnimation = openAnimation || openTransitionName;
|
||||
if (openAnimation === undefined && openTransitionName === undefined) {
|
||||
if (menuMode === 'horizontal') {
|
||||
menuOpenAnimation = 'slide-up';
|
||||
} else if (menuMode === 'inline') {
|
||||
menuOpenAnimation = animation;
|
||||
} else {
|
||||
// When mode switch from inline
|
||||
// submenu should hide without animation
|
||||
if (this.switchingModeFromInline) {
|
||||
menuOpenAnimation = '';
|
||||
this.switchingModeFromInline = false;
|
||||
} else {
|
||||
menuOpenAnimation = 'zoom-big';
|
||||
}
|
||||
}
|
||||
}
|
||||
return menuOpenAnimation;
|
||||
},
|
||||
},
|
||||
render() {
|
||||
const { layoutSiderContext } = this;
|
||||
const { collapsedWidth } = layoutSiderContext;
|
||||
const { getPopupContainer: getContextPopupContainer } = this.configProvider;
|
||||
const props = getOptionProps(this);
|
||||
const { prefixCls: customizePrefixCls, theme, getPopupContainer } = props;
|
||||
const getPrefixCls = this.configProvider.getPrefixCls;
|
||||
const prefixCls = getPrefixCls('menu', customizePrefixCls);
|
||||
const menuMode = this.getRealMenuMode();
|
||||
const menuOpenAnimation = this.getMenuOpenAnimation(menuMode);
|
||||
const { class: className, ...otherAttrs } = this.$attrs;
|
||||
const menuClassName = {
|
||||
[className as string]: className,
|
||||
[`${prefixCls}-${theme}`]: true,
|
||||
[`${prefixCls}-inline-collapsed`]: this.getInlineCollapsed(),
|
||||
};
|
||||
|
||||
const menuProps = {
|
||||
...omit(props, [
|
||||
'inlineCollapsed',
|
||||
'onUpdate:selectedKeys',
|
||||
'onUpdate:openKeys',
|
||||
'onSelectChange',
|
||||
]),
|
||||
getPopupContainer: getPopupContainer || getContextPopupContainer,
|
||||
openKeys: this.sOpenKeys,
|
||||
mode: menuMode,
|
||||
prefixCls,
|
||||
...otherAttrs,
|
||||
onSelect: this.handleSelect,
|
||||
onDeselect: this.handleDeselect,
|
||||
onOpenChange: this.handleOpenChange,
|
||||
onMouseenter: this.handleMouseEnter,
|
||||
onTransitionend: this.handleTransitionEnd,
|
||||
// children: getSlot(this),
|
||||
};
|
||||
if (!hasProp(this, 'selectedKeys')) {
|
||||
delete menuProps.selectedKeys;
|
||||
}
|
||||
|
||||
if (menuMode !== 'inline') {
|
||||
// closing vertical popup submenu after click it
|
||||
menuProps.onClick = this.handleClick;
|
||||
menuProps.openTransitionName = menuOpenAnimation;
|
||||
} else {
|
||||
menuProps.onClick = (e: Event) => {
|
||||
this.$emit('click', e);
|
||||
};
|
||||
menuProps.openAnimation = menuOpenAnimation;
|
||||
}
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/8587
|
||||
const hideMenu =
|
||||
this.getInlineCollapsed() &&
|
||||
(collapsedWidth === 0 || collapsedWidth === '0' || collapsedWidth === '0px');
|
||||
if (hideMenu) {
|
||||
menuProps.openKeys = [];
|
||||
}
|
||||
|
||||
return <VcMenu {...menuProps} class={menuClassName} v-slots={this.$slots} />;
|
||||
},
|
||||
});
|
||||
|
||||
import Menu, { MenuProps } from './src/Menu';
|
||||
import MenuItem, { MenuItemProps } from './src/MenuItem';
|
||||
import SubMenu, { SubMenuProps } from './src/SubMenu';
|
||||
import ItemGroup, { MenuItemGroupProps } from './src/ItemGroup';
|
||||
import Divider from './src/Divider';
|
||||
import { App, Plugin } from 'vue';
|
||||
/* istanbul ignore next */
|
||||
Menu.install = function(app: App) {
|
||||
app.component(Menu.name, Menu);
|
||||
app.component(Menu.Item.name, Menu.Item);
|
||||
app.component(Menu.SubMenu.name, Menu.SubMenu);
|
||||
app.component(Menu.Divider.name, Menu.Divider);
|
||||
app.component(Menu.ItemGroup.name, Menu.ItemGroup);
|
||||
app.component(MenuItem.name, MenuItem);
|
||||
app.component(SubMenu.name, SubMenu);
|
||||
app.component(Divider.name, Divider);
|
||||
app.component(ItemGroup.name, ItemGroup);
|
||||
return app;
|
||||
};
|
||||
|
||||
Menu.Item = MenuItem;
|
||||
Menu.Divider = Divider;
|
||||
Menu.SubMenu = SubMenu;
|
||||
Menu.ItemGroup = ItemGroup;
|
||||
|
||||
export {
|
||||
SubMenu,
|
||||
MenuItem as Item,
|
||||
MenuItem,
|
||||
ItemGroup,
|
||||
Divider,
|
||||
MenuProps,
|
||||
SubMenuProps,
|
||||
MenuItemProps,
|
||||
MenuItemGroupProps,
|
||||
};
|
||||
|
||||
export default Menu as typeof Menu &
|
||||
Plugin & {
|
||||
readonly Item: typeof Item;
|
||||
readonly Item: typeof MenuItem;
|
||||
readonly SubMenu: typeof SubMenu;
|
||||
readonly Divider: typeof Divider;
|
||||
readonly ItemGroup: typeof ItemGroup;
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Divider',
|
||||
props: {
|
||||
prefixCls: String,
|
||||
},
|
||||
setup(props) {
|
||||
return () => {
|
||||
return <li class={`${props.prefixCls}-item-divider`} />;
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
import { computed, defineComponent, ref, watch } from '@vue/runtime-core';
|
||||
import Transition from '../../_util/transition';
|
||||
import { useInjectMenu, MenuContextProvider } from './hooks/useMenuContext';
|
||||
import SubMenuList from './SubMenuList';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'InlineSubMenuList',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
id: String,
|
||||
open: Boolean,
|
||||
keyPath: Array,
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
const fixedMode = computed(() => 'inline');
|
||||
const { motion, mode, defaultMotions } = useInjectMenu();
|
||||
const sameModeRef = computed(() => mode.value === fixedMode.value);
|
||||
const destroy = ref(!sameModeRef.value);
|
||||
|
||||
const mergedOpen = computed(() => (sameModeRef.value ? props.open : false));
|
||||
|
||||
// ================================= Effect =================================
|
||||
// Reset destroy state when mode change back
|
||||
watch(
|
||||
mode,
|
||||
() => {
|
||||
if (sameModeRef.value) {
|
||||
destroy.value = false;
|
||||
}
|
||||
},
|
||||
{ flush: 'post' },
|
||||
);
|
||||
const style = ref({});
|
||||
const className = ref('');
|
||||
const mergedMotion = computed(() => {
|
||||
const m =
|
||||
motion.value || defaultMotions.value?.[fixedMode.value] || defaultMotions.value?.other;
|
||||
const res = typeof m === 'function' ? m(style, className) : m;
|
||||
return { ...res, appear: props.keyPath.length <= 1 };
|
||||
});
|
||||
return () => {
|
||||
if (destroy.value) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<MenuContextProvider
|
||||
props={{
|
||||
mode: fixedMode,
|
||||
locked: !sameModeRef.value,
|
||||
}}
|
||||
>
|
||||
<Transition {...mergedMotion.value}>
|
||||
<SubMenuList
|
||||
v-show={mergedOpen.value}
|
||||
id={props.id}
|
||||
style={style.value}
|
||||
class={className.value}
|
||||
>
|
||||
{slots.default?.()}
|
||||
</SubMenuList>
|
||||
</Transition>
|
||||
</MenuContextProvider>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
import { getPropsSlot } from '../../_util/props-util';
|
||||
import { computed, defineComponent, ExtractPropTypes } from 'vue';
|
||||
import PropTypes from '../../_util/vue-types';
|
||||
import { useInjectMenu } from './hooks/useMenuContext';
|
||||
|
||||
const menuItemGroupProps = {
|
||||
title: PropTypes.VNodeChild,
|
||||
};
|
||||
|
||||
export type MenuItemGroupProps = Partial<ExtractPropTypes<typeof menuItemGroupProps>>;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AMenuItemGroup',
|
||||
inheritAttrs: false,
|
||||
props: menuItemGroupProps,
|
||||
slots: ['title'],
|
||||
setup(props, { slots, attrs }) {
|
||||
const { prefixCls } = useInjectMenu();
|
||||
const groupPrefixCls = computed(() => `${prefixCls.value}-item-group`);
|
||||
return () => {
|
||||
return (
|
||||
<li {...attrs} onClick={e => e.stopPropagation()} class={groupPrefixCls.value}>
|
||||
<div
|
||||
title={typeof props.title === 'string' ? props.title : undefined}
|
||||
class={`${groupPrefixCls.value}-title`}
|
||||
>
|
||||
{getPropsSlot(slots, props, 'title')}
|
||||
</div>
|
||||
<ul class={`${groupPrefixCls.value}-list`}>{slots.default?.()}</ul>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,448 @@
|
|||
import { Key } from '../../_util/type';
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
ExtractPropTypes,
|
||||
ref,
|
||||
PropType,
|
||||
inject,
|
||||
watchEffect,
|
||||
watch,
|
||||
onMounted,
|
||||
unref,
|
||||
UnwrapRef,
|
||||
} from 'vue';
|
||||
import shallowEqual from '../../_util/shallowequal';
|
||||
import useProvideMenu, {
|
||||
MenuContextProvider,
|
||||
StoreMenuInfo,
|
||||
useProvideFirstLevel,
|
||||
} from './hooks/useMenuContext';
|
||||
import useConfigInject from '../../_util/hooks/useConfigInject';
|
||||
import {
|
||||
MenuTheme,
|
||||
MenuMode,
|
||||
BuiltinPlacements,
|
||||
TriggerSubMenuAction,
|
||||
MenuInfo,
|
||||
SelectInfo,
|
||||
} from './interface';
|
||||
import devWarning from '../../vc-util/devWarning';
|
||||
import { collapseMotion, CSSMotionProps } from '../../_util/transition';
|
||||
import uniq from 'lodash-es/uniq';
|
||||
import { SiderCollapsedKey } from '../../layout/injectionKey';
|
||||
import { flattenChildren } from '../../_util/props-util';
|
||||
import Overflow from '../../vc-overflow';
|
||||
import MenuItem from './MenuItem';
|
||||
import SubMenu from './SubMenu';
|
||||
import EllipsisOutlined from '@ant-design/icons-vue/EllipsisOutlined';
|
||||
|
||||
export const menuProps = {
|
||||
prefixCls: String,
|
||||
disabled: Boolean,
|
||||
inlineCollapsed: Boolean,
|
||||
disabledOverflow: Boolean,
|
||||
openKeys: Array,
|
||||
selectedKeys: Array,
|
||||
activeKey: String, // 内部组件使用
|
||||
selectable: { type: Boolean, default: true },
|
||||
multiple: { type: Boolean, default: false },
|
||||
|
||||
motion: Object as PropType<CSSMotionProps>,
|
||||
|
||||
theme: { type: String as PropType<MenuTheme>, default: 'light' },
|
||||
mode: { type: String as PropType<MenuMode>, default: 'vertical' },
|
||||
|
||||
inlineIndent: { type: Number, default: 24 },
|
||||
subMenuOpenDelay: { type: Number, default: 0.1 },
|
||||
subMenuCloseDelay: { type: Number, default: 0.1 },
|
||||
|
||||
builtinPlacements: { type: Object as PropType<BuiltinPlacements> },
|
||||
|
||||
triggerSubMenuAction: { type: String as PropType<TriggerSubMenuAction>, default: 'hover' },
|
||||
|
||||
getPopupContainer: Function as PropType<(node: HTMLElement) => HTMLElement>,
|
||||
};
|
||||
|
||||
export type MenuProps = Partial<ExtractPropTypes<typeof menuProps>>;
|
||||
|
||||
const EMPTY_LIST: string[] = [];
|
||||
export default defineComponent({
|
||||
name: 'AMenu',
|
||||
props: menuProps,
|
||||
emits: [
|
||||
'update:openKeys',
|
||||
'openChange',
|
||||
'select',
|
||||
'deselect',
|
||||
'update:selectedKeys',
|
||||
'click',
|
||||
'update:activeKey',
|
||||
],
|
||||
setup(props, { slots, emit }) {
|
||||
const { prefixCls, direction } = useConfigInject('menu', props);
|
||||
const store = ref<Record<string, StoreMenuInfo>>({});
|
||||
const siderCollapsed = inject(SiderCollapsedKey, ref(undefined));
|
||||
const inlineCollapsed = computed(() => {
|
||||
if (siderCollapsed.value !== undefined) {
|
||||
return siderCollapsed.value;
|
||||
}
|
||||
return props.inlineCollapsed;
|
||||
});
|
||||
|
||||
const isMounted = ref(false);
|
||||
onMounted(() => {
|
||||
isMounted.value = true;
|
||||
});
|
||||
watchEffect(() => {
|
||||
devWarning(
|
||||
!(props.inlineCollapsed === true && props.mode !== 'inline'),
|
||||
'Menu',
|
||||
'`inlineCollapsed` should only be used when `mode` is inline.',
|
||||
);
|
||||
|
||||
devWarning(
|
||||
!(siderCollapsed.value !== undefined && props.inlineCollapsed === true),
|
||||
'Menu',
|
||||
'`inlineCollapsed` not control Menu under Sider. Should set `collapsed` on Sider instead.',
|
||||
);
|
||||
});
|
||||
|
||||
const activeKeys = ref([]);
|
||||
const mergedSelectedKeys = ref([]);
|
||||
const keyMapStore = ref({});
|
||||
watch(
|
||||
store,
|
||||
() => {
|
||||
const newKeyMapStore = {};
|
||||
for (const menuInfo of Object.values(store.value)) {
|
||||
newKeyMapStore[menuInfo.key] = menuInfo;
|
||||
}
|
||||
keyMapStore.value = newKeyMapStore;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
watchEffect(() => {
|
||||
if ('activeKey' in props) {
|
||||
let keys = [];
|
||||
const menuInfo = props.activeKey
|
||||
? (keyMapStore.value[props.activeKey] as UnwrapRef<StoreMenuInfo>)
|
||||
: undefined;
|
||||
if (menuInfo && props.activeKey !== undefined) {
|
||||
keys = [...menuInfo.parentKeys, props.activeKey];
|
||||
} else {
|
||||
keys = [];
|
||||
}
|
||||
if (!shallowEqual(activeKeys.value, keys)) {
|
||||
activeKeys.value = keys;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.selectedKeys,
|
||||
selectedKeys => {
|
||||
mergedSelectedKeys.value = selectedKeys || mergedSelectedKeys.value;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const selectedSubMenuEventKeys = ref([]);
|
||||
|
||||
watch(
|
||||
[keyMapStore, mergedSelectedKeys],
|
||||
() => {
|
||||
let subMenuParentEventKeys = [];
|
||||
mergedSelectedKeys.value.forEach(key => {
|
||||
const menuInfo = keyMapStore.value[key];
|
||||
if (menuInfo) {
|
||||
subMenuParentEventKeys.push(...unref(menuInfo.parentEventKeys));
|
||||
}
|
||||
});
|
||||
|
||||
subMenuParentEventKeys = uniq(subMenuParentEventKeys);
|
||||
if (!shallowEqual(selectedSubMenuEventKeys.value, subMenuParentEventKeys)) {
|
||||
selectedSubMenuEventKeys.value = subMenuParentEventKeys;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// >>>>> Trigger select
|
||||
const triggerSelection = (info: MenuInfo) => {
|
||||
if (!props.selectable) {
|
||||
return;
|
||||
}
|
||||
// Insert or Remove
|
||||
const { key: targetKey } = info;
|
||||
const exist = mergedSelectedKeys.value.includes(targetKey);
|
||||
let newSelectedKeys: Key[];
|
||||
|
||||
if (props.multiple) {
|
||||
if (exist) {
|
||||
newSelectedKeys = mergedSelectedKeys.value.filter(key => key !== targetKey);
|
||||
} else {
|
||||
newSelectedKeys = [...mergedSelectedKeys.value, targetKey];
|
||||
}
|
||||
} else {
|
||||
newSelectedKeys = [targetKey];
|
||||
}
|
||||
|
||||
// Trigger event
|
||||
const selectInfo: SelectInfo = {
|
||||
...info,
|
||||
selectedKeys: newSelectedKeys,
|
||||
};
|
||||
if (!shallowEqual(newSelectedKeys, mergedSelectedKeys.value)) {
|
||||
if (!('selectedKeys' in props)) {
|
||||
mergedSelectedKeys.value = newSelectedKeys;
|
||||
}
|
||||
emit('update:selectedKeys', newSelectedKeys);
|
||||
if (exist && props.multiple) {
|
||||
emit('deselect', selectInfo);
|
||||
} else {
|
||||
emit('select', selectInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (mergedMode.value !== 'inline' && !props.multiple && mergedOpenKeys.value.length) {
|
||||
triggerOpenKeys(EMPTY_LIST);
|
||||
}
|
||||
};
|
||||
|
||||
const mergedOpenKeys = ref([]);
|
||||
|
||||
watch(
|
||||
() => props.openKeys,
|
||||
(openKeys = mergedOpenKeys.value) => {
|
||||
if (!shallowEqual(mergedOpenKeys.value, openKeys)) {
|
||||
mergedOpenKeys.value = openKeys;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const changeActiveKeys = (keys: Key[]) => {
|
||||
if ('activeKey' in props) {
|
||||
emit('update:activeKey', keys[keys.length - 1]);
|
||||
} else {
|
||||
activeKeys.value = keys;
|
||||
}
|
||||
};
|
||||
const disabled = computed(() => !!props.disabled);
|
||||
const isRtl = computed(() => direction.value === 'rtl');
|
||||
const mergedMode = ref<MenuMode>('vertical');
|
||||
const mergedInlineCollapsed = ref(false);
|
||||
|
||||
watchEffect(() => {
|
||||
if ((props.mode === 'inline' || props.mode === 'vertical') && inlineCollapsed.value) {
|
||||
mergedMode.value = 'vertical';
|
||||
mergedInlineCollapsed.value = inlineCollapsed.value;
|
||||
} else {
|
||||
mergedMode.value = props.mode;
|
||||
mergedInlineCollapsed.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
const isInlineMode = computed(() => mergedMode.value === 'inline');
|
||||
|
||||
const triggerOpenKeys = (keys: string[]) => {
|
||||
mergedOpenKeys.value = keys;
|
||||
emit('update:openKeys', keys);
|
||||
emit('openChange', keys);
|
||||
};
|
||||
|
||||
// >>>>> Cache & Reset open keys when inlineCollapsed changed
|
||||
const inlineCacheOpenKeys = ref(mergedOpenKeys.value);
|
||||
|
||||
const mountRef = ref(false);
|
||||
|
||||
// Cache
|
||||
watch(
|
||||
mergedOpenKeys,
|
||||
() => {
|
||||
if (isInlineMode.value) {
|
||||
inlineCacheOpenKeys.value = mergedOpenKeys.value;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// Restore
|
||||
watch(
|
||||
isInlineMode,
|
||||
() => {
|
||||
if (!mountRef.value) {
|
||||
mountRef.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInlineMode.value) {
|
||||
mergedOpenKeys.value = inlineCacheOpenKeys.value;
|
||||
} else {
|
||||
// Trigger open event in case its in control
|
||||
triggerOpenKeys(EMPTY_LIST);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const className = computed(() => {
|
||||
return {
|
||||
[`${prefixCls.value}`]: true,
|
||||
[`${prefixCls.value}-root`]: true,
|
||||
[`${prefixCls.value}-${mergedMode.value}`]: true,
|
||||
[`${prefixCls.value}-inline-collapsed`]: mergedInlineCollapsed.value,
|
||||
[`${prefixCls.value}-rtl`]: isRtl.value,
|
||||
[`${prefixCls.value}-${props.theme}`]: true,
|
||||
};
|
||||
});
|
||||
|
||||
const defaultMotions = {
|
||||
horizontal: { name: `ant-slide-up` },
|
||||
inline: collapseMotion,
|
||||
other: { name: `ant-zoom-big` },
|
||||
};
|
||||
|
||||
useProvideFirstLevel(true);
|
||||
|
||||
const getChildrenKeys = (eventKeys: string[] = []): Key[] => {
|
||||
const keys = [];
|
||||
const storeValue = store.value;
|
||||
eventKeys.forEach(eventKey => {
|
||||
const { key, childrenEventKeys } = storeValue[eventKey];
|
||||
keys.push(key, ...getChildrenKeys(childrenEventKeys));
|
||||
});
|
||||
return keys;
|
||||
};
|
||||
|
||||
// ========================= Open =========================
|
||||
/**
|
||||
* Click for item. SubMenu do not have selection status
|
||||
*/
|
||||
const onInternalClick = (info: MenuInfo) => {
|
||||
emit('click', info);
|
||||
triggerSelection(info);
|
||||
};
|
||||
|
||||
const onInternalOpenChange = (eventKey: Key, open: boolean) => {
|
||||
const { key, childrenEventKeys } = store.value[eventKey];
|
||||
let newOpenKeys = mergedOpenKeys.value.filter(k => k !== key);
|
||||
|
||||
if (open) {
|
||||
newOpenKeys.push(key);
|
||||
} else if (mergedMode.value !== 'inline') {
|
||||
// We need find all related popup to close
|
||||
const subPathKeys = getChildrenKeys(childrenEventKeys);
|
||||
newOpenKeys = newOpenKeys.filter(k => !subPathKeys.includes(k));
|
||||
}
|
||||
|
||||
if (!shallowEqual(mergedOpenKeys, newOpenKeys)) {
|
||||
triggerOpenKeys(newOpenKeys);
|
||||
}
|
||||
};
|
||||
|
||||
const registerMenuInfo = (key: string, info: StoreMenuInfo) => {
|
||||
store.value = { ...store.value, [key]: info as any };
|
||||
};
|
||||
const unRegisterMenuInfo = (key: string) => {
|
||||
delete store.value[key];
|
||||
store.value = { ...store.value };
|
||||
};
|
||||
|
||||
const lastVisibleIndex = ref(0);
|
||||
|
||||
useProvideMenu({
|
||||
store,
|
||||
prefixCls,
|
||||
activeKeys,
|
||||
openKeys: mergedOpenKeys,
|
||||
selectedKeys: mergedSelectedKeys,
|
||||
changeActiveKeys,
|
||||
disabled,
|
||||
rtl: isRtl,
|
||||
mode: mergedMode,
|
||||
inlineIndent: computed(() => props.inlineIndent),
|
||||
subMenuCloseDelay: computed(() => props.subMenuCloseDelay),
|
||||
subMenuOpenDelay: computed(() => props.subMenuOpenDelay),
|
||||
builtinPlacements: computed(() => props.builtinPlacements),
|
||||
triggerSubMenuAction: computed(() => props.triggerSubMenuAction),
|
||||
getPopupContainer: computed(() => props.getPopupContainer),
|
||||
inlineCollapsed: mergedInlineCollapsed,
|
||||
antdMenuTheme: computed(() => props.theme),
|
||||
siderCollapsed,
|
||||
defaultMotions: computed(() => (isMounted.value ? defaultMotions : null)),
|
||||
motion: computed(() => (isMounted.value ? props.motion : null)),
|
||||
overflowDisabled: computed(() => props.disabledOverflow),
|
||||
onOpenChange: onInternalOpenChange,
|
||||
onItemClick: onInternalClick,
|
||||
registerMenuInfo,
|
||||
unRegisterMenuInfo,
|
||||
selectedSubMenuEventKeys,
|
||||
isRootMenu: true,
|
||||
});
|
||||
return () => {
|
||||
const childList = flattenChildren(slots.default?.());
|
||||
const allVisible =
|
||||
lastVisibleIndex.value >= childList.length - 1 ||
|
||||
mergedMode.value !== 'horizontal' ||
|
||||
props.disabledOverflow;
|
||||
// >>>>> Children
|
||||
const wrappedChildList =
|
||||
mergedMode.value !== 'horizontal' || props.disabledOverflow
|
||||
? childList
|
||||
: // Need wrap for overflow dropdown that do not response for open
|
||||
childList.map((child, index) => (
|
||||
// Always wrap provider to avoid sub node re-mount
|
||||
<MenuContextProvider
|
||||
key={child.key}
|
||||
props={{ overflowDisabled: computed(() => index > lastVisibleIndex.value) }}
|
||||
>
|
||||
{child}
|
||||
</MenuContextProvider>
|
||||
));
|
||||
const overflowedIndicator = <EllipsisOutlined />;
|
||||
|
||||
// data-hack-store-update 初步判断是 vue bug,先用hack方式
|
||||
return (
|
||||
<Overflow
|
||||
data-hack-store-update={store.value}
|
||||
prefixCls={`${prefixCls.value}-overflow`}
|
||||
component="ul"
|
||||
itemComponent={MenuItem}
|
||||
class={className.value}
|
||||
role="menu"
|
||||
data={wrappedChildList}
|
||||
renderRawItem={node => node}
|
||||
renderRawRest={omitItems => {
|
||||
// We use origin list since wrapped list use context to prevent open
|
||||
const len = omitItems.length;
|
||||
|
||||
const originOmitItems = len ? childList.slice(-len) : null;
|
||||
|
||||
return (
|
||||
<SubMenu
|
||||
eventKey={Overflow.OVERFLOW_KEY}
|
||||
title={overflowedIndicator}
|
||||
disabled={allVisible}
|
||||
internalPopupClose={len === 0}
|
||||
>
|
||||
{originOmitItems}
|
||||
</SubMenu>
|
||||
);
|
||||
}}
|
||||
maxCount={
|
||||
mergedMode.value !== 'horizontal' || props.disabledOverflow
|
||||
? Overflow.INVALIDATE
|
||||
: Overflow.RESPONSIVE
|
||||
}
|
||||
ssr="full"
|
||||
data-menu-list
|
||||
onVisibleChange={newLastIndex => {
|
||||
lastVisibleIndex.value = newLastIndex;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,237 @@
|
|||
import { flattenChildren, getPropsSlot, isValidElement } from '../../_util/props-util';
|
||||
import PropTypes from '../../_util/vue-types';
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
ExtractPropTypes,
|
||||
getCurrentInstance,
|
||||
onBeforeUnmount,
|
||||
ref,
|
||||
watch,
|
||||
} from 'vue';
|
||||
import { useInjectKeyPath } from './hooks/useKeyPath';
|
||||
import { useInjectFirstLevel, useInjectMenu } from './hooks/useMenuContext';
|
||||
import { cloneElement } from '../../_util/vnode';
|
||||
import Tooltip from '../../tooltip';
|
||||
import { MenuInfo } from './interface';
|
||||
import KeyCode from '../../_util/KeyCode';
|
||||
import useDirectionStyle from './hooks/useDirectionStyle';
|
||||
import Overflow from '../../vc-overflow';
|
||||
|
||||
let indexGuid = 0;
|
||||
const menuItemProps = {
|
||||
role: String,
|
||||
disabled: Boolean,
|
||||
danger: Boolean,
|
||||
title: { type: [String, Boolean], default: undefined },
|
||||
icon: PropTypes.VNodeChild,
|
||||
};
|
||||
|
||||
export type MenuItemProps = Partial<ExtractPropTypes<typeof menuItemProps>>;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AMenuItem',
|
||||
inheritAttrs: false,
|
||||
props: menuItemProps,
|
||||
emits: ['mouseenter', 'mouseleave', 'click', 'keydown', 'focus'],
|
||||
slots: ['icon', 'title'],
|
||||
setup(props, { slots, emit, attrs }) {
|
||||
const instance = getCurrentInstance();
|
||||
const key = instance.vnode.key;
|
||||
const eventKey = `menu_item_${++indexGuid}_$$_${key}`;
|
||||
const { parentEventKeys, parentKeys } = useInjectKeyPath();
|
||||
const {
|
||||
prefixCls,
|
||||
activeKeys,
|
||||
disabled,
|
||||
changeActiveKeys,
|
||||
rtl,
|
||||
inlineCollapsed,
|
||||
siderCollapsed,
|
||||
onItemClick,
|
||||
selectedKeys,
|
||||
registerMenuInfo,
|
||||
unRegisterMenuInfo,
|
||||
} = useInjectMenu();
|
||||
const firstLevel = useInjectFirstLevel();
|
||||
const isActive = ref(false);
|
||||
const keysPath = computed(() => {
|
||||
return [...parentKeys.value, key];
|
||||
});
|
||||
|
||||
// const keysPath = computed(() => [...parentEventKeys.value, eventKey]);
|
||||
const menuInfo = {
|
||||
eventKey,
|
||||
key,
|
||||
parentEventKeys,
|
||||
parentKeys,
|
||||
isLeaf: true,
|
||||
};
|
||||
|
||||
registerMenuInfo(eventKey, menuInfo);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unRegisterMenuInfo(eventKey);
|
||||
});
|
||||
|
||||
watch(
|
||||
activeKeys,
|
||||
() => {
|
||||
isActive.value = !!activeKeys.value.find(val => val === key);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
const mergedDisabled = computed(() => disabled.value || props.disabled);
|
||||
const selected = computed(() => selectedKeys.value.includes(key));
|
||||
const classNames = computed(() => {
|
||||
const itemCls = `${prefixCls.value}-item`;
|
||||
return {
|
||||
[`${itemCls}`]: true,
|
||||
[`${itemCls}-danger`]: props.danger,
|
||||
[`${itemCls}-active`]: isActive.value,
|
||||
[`${itemCls}-selected`]: selected.value,
|
||||
[`${itemCls}-disabled`]: mergedDisabled.value,
|
||||
};
|
||||
});
|
||||
|
||||
const getEventInfo = (e: MouseEvent | KeyboardEvent): MenuInfo => {
|
||||
return {
|
||||
key,
|
||||
eventKey,
|
||||
keyPath: keysPath.value,
|
||||
eventKeyPath: [...parentEventKeys.value, eventKey],
|
||||
domEvent: e,
|
||||
};
|
||||
};
|
||||
|
||||
// ============================ Events ============================
|
||||
const onInternalClick = (e: MouseEvent) => {
|
||||
if (mergedDisabled.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const info = getEventInfo(e);
|
||||
emit('click', e);
|
||||
onItemClick(info);
|
||||
};
|
||||
|
||||
const onMouseEnter = (event: MouseEvent) => {
|
||||
if (!mergedDisabled.value) {
|
||||
changeActiveKeys(keysPath.value);
|
||||
emit('mouseenter', event);
|
||||
}
|
||||
};
|
||||
const onMouseLeave = (event: MouseEvent) => {
|
||||
if (!mergedDisabled.value) {
|
||||
changeActiveKeys([]);
|
||||
emit('mouseleave', event);
|
||||
}
|
||||
};
|
||||
|
||||
const onInternalKeyDown = (e: KeyboardEvent) => {
|
||||
emit('keydown', e);
|
||||
|
||||
if (e.which === KeyCode.ENTER) {
|
||||
const info = getEventInfo(e);
|
||||
|
||||
// Legacy. Key will also trigger click event
|
||||
emit('click', e);
|
||||
onItemClick(info);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Used for accessibility. Helper will focus element without key board.
|
||||
* We should manually trigger an active
|
||||
*/
|
||||
const onInternalFocus = (e: FocusEvent) => {
|
||||
changeActiveKeys(keysPath.value);
|
||||
emit('focus', e);
|
||||
};
|
||||
|
||||
const renderItemChildren = (icon: any, children: any) => {
|
||||
const wrapNode = <span class={`${prefixCls.value}-title-content`}>{children}</span>;
|
||||
// inline-collapsed.md demo 依赖 span 来隐藏文字,有 icon 属性,则内部包裹一个 span
|
||||
// ref: https://github.com/ant-design/ant-design/pull/23456
|
||||
if (!icon || (isValidElement(children) && children.type === 'span')) {
|
||||
if (children && inlineCollapsed.value && firstLevel && typeof children === 'string') {
|
||||
return (
|
||||
<div class={`${prefixCls.value}-inline-collapsed-noicon`}>{children.charAt(0)}</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return wrapNode;
|
||||
};
|
||||
|
||||
// ========================== DirectionStyle ==========================
|
||||
const directionStyle = useDirectionStyle(computed(() => keysPath.value.length));
|
||||
|
||||
return () => {
|
||||
const title = props.title ?? slots.title?.();
|
||||
const children = flattenChildren(slots.default?.());
|
||||
const childrenLength = children.length;
|
||||
let tooltipTitle: any = title;
|
||||
if (typeof title === 'undefined') {
|
||||
tooltipTitle = firstLevel ? children : '';
|
||||
} else if (title === false) {
|
||||
tooltipTitle = '';
|
||||
}
|
||||
const tooltipProps: any = {
|
||||
title: tooltipTitle,
|
||||
};
|
||||
|
||||
if (!siderCollapsed.value && !inlineCollapsed.value) {
|
||||
tooltipProps.title = null;
|
||||
// Reset `visible` to fix control mode tooltip display not correct
|
||||
// ref: https://github.com/ant-design/ant-design/issues/16742
|
||||
tooltipProps.visible = false;
|
||||
}
|
||||
|
||||
// ============================ Render ============================
|
||||
const optionRoleProps = {};
|
||||
|
||||
if (props.role === 'option') {
|
||||
optionRoleProps['aria-selected'] = selected.value;
|
||||
}
|
||||
|
||||
const icon = getPropsSlot(slots, props, 'icon');
|
||||
return (
|
||||
<Tooltip
|
||||
{...tooltipProps}
|
||||
placement={rtl.value ? 'left' : 'right'}
|
||||
overlayClassName={`${prefixCls.value}-inline-collapsed-tooltip`}
|
||||
>
|
||||
<Overflow.Item
|
||||
component="li"
|
||||
{...attrs}
|
||||
style={{ ...((attrs.style as any) || {}), ...directionStyle.value }}
|
||||
class={[
|
||||
classNames.value,
|
||||
{
|
||||
[`${attrs.class}`]: !!attrs.class,
|
||||
[`${prefixCls.value}-item-only-child`]:
|
||||
(icon ? childrenLength + 1 : childrenLength) === 1,
|
||||
},
|
||||
]}
|
||||
role={props.role || 'menuitem'}
|
||||
tabindex={props.disabled ? null : -1}
|
||||
data-menu-id={key}
|
||||
aria-disabled={props.disabled}
|
||||
{...optionRoleProps}
|
||||
onMouseenter={onMouseEnter}
|
||||
onMouseleave={onMouseLeave}
|
||||
onClick={onInternalClick}
|
||||
onKeydown={onInternalKeyDown}
|
||||
onFocus={onInternalFocus}
|
||||
title={typeof title === 'string' ? title : undefined}
|
||||
>
|
||||
{cloneElement(icon, {
|
||||
class: `${prefixCls.value}-item-icon`,
|
||||
})}
|
||||
{renderItemChildren(icon, children)}
|
||||
</Overflow.Item>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,103 @@
|
|||
import Trigger from '../../vc-trigger';
|
||||
import { computed, defineComponent, onBeforeUnmount, PropType, ref, watch } from 'vue';
|
||||
import { MenuMode } from './interface';
|
||||
import { useInjectMenu } from './hooks/useMenuContext';
|
||||
import { placements, placementsRtl } from './placements';
|
||||
import raf from '../../_util/raf';
|
||||
import classNames from '../../_util/classNames';
|
||||
|
||||
const popupPlacementMap = {
|
||||
horizontal: 'bottomLeft',
|
||||
vertical: 'rightTop',
|
||||
'vertical-left': 'rightTop',
|
||||
'vertical-right': 'leftTop',
|
||||
};
|
||||
export default defineComponent({
|
||||
name: 'PopupTrigger',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
prefixCls: String,
|
||||
mode: String as PropType<MenuMode>,
|
||||
visible: Boolean,
|
||||
// popup: React.ReactNode;
|
||||
popupClassName: String,
|
||||
popupOffset: Array as PropType<number[]>,
|
||||
disabled: Boolean,
|
||||
onVisibleChange: Function as PropType<(visible: boolean) => void>,
|
||||
},
|
||||
slots: ['popup'],
|
||||
emits: ['visibleChange'],
|
||||
setup(props, { slots, emit }) {
|
||||
const innerVisible = ref(false);
|
||||
const {
|
||||
getPopupContainer,
|
||||
rtl,
|
||||
subMenuOpenDelay,
|
||||
subMenuCloseDelay,
|
||||
builtinPlacements,
|
||||
triggerSubMenuAction,
|
||||
isRootMenu,
|
||||
} = useInjectMenu();
|
||||
|
||||
const placement = computed(() =>
|
||||
rtl.value
|
||||
? { ...placementsRtl, ...builtinPlacements.value }
|
||||
: { ...placements, ...builtinPlacements.value },
|
||||
);
|
||||
|
||||
const popupPlacement = computed(() => popupPlacementMap[props.mode]);
|
||||
|
||||
const visibleRef = ref<number>();
|
||||
watch(
|
||||
() => props.visible,
|
||||
visible => {
|
||||
raf.cancel(visibleRef.value);
|
||||
visibleRef.value = raf(() => {
|
||||
innerVisible.value = visible;
|
||||
});
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
onBeforeUnmount(() => {
|
||||
raf.cancel(visibleRef.value);
|
||||
});
|
||||
|
||||
const onVisibleChange = (visible: boolean) => {
|
||||
emit('visibleChange', visible);
|
||||
};
|
||||
return () => {
|
||||
const { prefixCls, popupClassName, mode, popupOffset, disabled } = props;
|
||||
return (
|
||||
<Trigger
|
||||
prefixCls={prefixCls}
|
||||
popupClassName={classNames(
|
||||
`${prefixCls}-popup`,
|
||||
{
|
||||
[`${prefixCls}-rtl`]: rtl.value,
|
||||
},
|
||||
popupClassName,
|
||||
)}
|
||||
stretch={mode === 'horizontal' ? 'minWidth' : null}
|
||||
getPopupContainer={
|
||||
isRootMenu ? getPopupContainer.value : triggerNode => triggerNode.parentNode
|
||||
}
|
||||
builtinPlacements={placement.value}
|
||||
popupPlacement={popupPlacement.value}
|
||||
popupVisible={innerVisible.value}
|
||||
popupAlign={popupOffset && { offset: popupOffset }}
|
||||
action={disabled ? [] : [triggerSubMenuAction.value]}
|
||||
mouseEnterDelay={subMenuOpenDelay.value}
|
||||
mouseLeaveDelay={subMenuCloseDelay.value}
|
||||
onPopupVisibleChange={onVisibleChange}
|
||||
forceRender={true}
|
||||
v-slots={{
|
||||
popup: () => {
|
||||
return slots.popup?.({ visible: innerVisible.value });
|
||||
},
|
||||
default: slots.default,
|
||||
}}
|
||||
></Trigger>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue