ant-design-vue/components/typography/Base.tsx
tangjinzhou 3aeeeb2aed
3.0 ready (#4523)
* refactor: transfer、tooltip (#4306)

* refactor(transfer): use composition api (#4135)

* refactor(transfer): use composition api

* fix: remove console

* refactor(tooltip): use composition api (#4059)

* refactor(tooltip): use composition api

* chore: useConfigInject

* fix: remove useless

* style: format code

* refactor: transfer

* refactor: tooltip

Co-authored-by: ajuner <106791576@qq.com>

* Refactor mentions (#4341)

* refactor(mentions): use compositionAPI (#4313)

* refactor: mentions

* refactor: mentions

Co-authored-by: ajuner <106791576@qq.com>

* Refactor progress (#4358)

* fix: timepicker error border not show #4331

* fix(UploadDragger): fix UploadDrager no export (#4334)

* refactor(switch): support customize checked value #4329 (#4332)

* refactor(switch): support customize checked value #4329

* test: add test case

* refactor: update props name

* refactor: update ts

* refactor: optimize

* style: uncheckedValue to unCheckedValue

* test: update snap

* feat: udpate switch ts

* docs: remove ie11

* fix: tree-select throw error when use slot title

* fix: TypeScript definition of Table interface for typescript 4.3.5 (#4353)

* fix type for typescript 4.3.5

* Update interface.ts

close #4296

* fix: dropdown submenu style error #4351
close #4351

* fix(notification): 完善notification类型 (#4346)

* refactor(progress): use composition API (#4355)

* refactor(progress): use composition API

* refactor(vc-progress): update

* refactor: progress

* refactor: progress

* fix: timepicker error border not show #4331

* fix(UploadDragger): fix UploadDrager no export (#4334)

* refactor(switch): support customize checked value #4329 (#4332)

* refactor(switch): support customize checked value #4329

* test: add test case

* refactor: update props name

* refactor: update ts

* refactor: optimize

* style: uncheckedValue to unCheckedValue

* test: update snap

* feat: udpate switch ts

* docs: remove ie11

* fix: tree-select throw error when use slot title

* fix: TypeScript definition of Table interface for typescript 4.3.5 (#4353)

* fix type for typescript 4.3.5

* Update interface.ts

close #4296

* fix: dropdown submenu style error #4351
close #4351

* fix(notification): 完善notification类型 (#4346)

* refactor(progress): use composition API (#4355)

* refactor(progress): use composition API

* refactor(vc-progress): update

* refactor: progress

* refactor: progress

Co-authored-by: Jarvis <35361626+fanhaoyuan@users.noreply.github.com>
Co-authored-by: John <John60676@qq.com>
Co-authored-by: 艾斯特洛 <axetroy.dev@gmail.com>
Co-authored-by: zanllp <qc@zanllp.cn>

* docs: add changelog

* refactor: tree

* refactor: tree

* style: lint

* refactor: tree

* 热factor: tree

* refactor: tree

* refactor: tree

* refactor: tree

* refactor: directory tree

* refactor: tree

* refactor: tree-select

* refactor: tree-select

* refactor: tree-select

* refactor: tree-select

* refactor: tree-select

* style: lint format

* refactor: tree-select

* refactor: tree-select

* refactor: tree-select

* refactor: tree-select

* refactor: tree-select

* refactor: tree-select

* fix: upload ts error

* fix: update tree title render & switchIcon

* test: update tree test

* feat: add VirtualScroll tree

* refactor: datePicker & calendar & trigger (#4522)

* style: update

* test: update calendar test

* test: update test

* test: update test

* refactor: slider

* feat: update slider css

* refactor: slider to ts

* refactor: slider to ts

* perf: update default itemHeight

* test: update

* fix: uddate ts type

* fix: update skeleton

* fix: update skeleton

* refactor: update vc-pagination

* refactor: pagination

* refactor: timeline

* refactor: steps

* refactor: collapse

* refactor: collapse

* refactor: popconfirm

* refactor: popover

* refactor: dropdown

* doc: merge doc

* chore: vite for dev (#4602)

* style: js to jsx

* doc: add site

* style: lint

* style: format ts type

* doc: update

* style: format code

* style: format site

* doc: update

* style: dmeo

* style: format scripts

* chore: remove sub-modules

* chore: update vite

* site: add site build

* test: update snap

* doc(select): add tip (#4606)

* refactor: table (#4641)

* refactor: table

* refactor: table

* refactor: table

* refactor: table

* refactor: table

* refactor: table

* refactor: table

* refactor: table

* refactor: table

* fix: column not pass to cell

* doc: uppate table

* fix: update bodyCell headerCell

* doc: remove examples

* refactor: table

* fix: table title not work

* fix: table selection

* fix: table checkStrictly

* refactor: table

* fix: table template error

* feat: table support summary

* test: update snap

* perf: table

* docs(table): fix ajax demo (#4639)

* test: update table

* refactor: remove old table

* doc: update  table doc

* doc: update doc

* doc: update select

* doc: update summary

Co-authored-by: John <John60676@qq.com>

* doc: update doc

* fix: menu arrow not work

* test: update

* doc: add next site

* style: format

* doc: update

* doc: update site script

* fix: expand icon not fixed

* feat: use renderSlot

* test: update table snap

* feat: confirm support reactively

* feat: configProvider.config

* feat: message support configprovider.config

* feat: notification support configprovider.config

* doc: update doc

* fix: typescript compile error

* style: add import eslint

* doc: update demo

* chore: set transpileOnly true

* style: fix eslint error

* test: update snap

* doc: update

* test: mock date

* test: update snap

* chore: remove gulp-typescript (#4675)

* feat: V3 form (#4678)

* chore: update husky

* perf: update formItem

* perf: useInjectFormItemContext

* fix: table ts error

* doc: add Customized Form Controls demo

* feat: export useInjectFormItemContext

* doc: update form doc

* doc: update doc

* doc: update doc

* feat: autocomplete support option slot

* doc: update

* feat: add form item rest

* style: remove omit.js

* refactor: autocomplete

* doc: add changelog to site

* doc: update site anchor

* doc: update doc layout

* test: update table test

* doc: update

* chore: udpate gulp script

* chore: udpate gulp script

* doc: add changelog

* doc: update

* test: ignore some test wait vue-test-utils

* fix: form id error #4582
close #4582

* doc: add select Responsive demo

* doc: remove temp doc

Co-authored-by: ajuner <106791576@qq.com>
Co-authored-by: Jarvis <35361626+fanhaoyuan@users.noreply.github.com>
Co-authored-by: John <John60676@qq.com>
Co-authored-by: 艾斯特洛 <axetroy.dev@gmail.com>
Co-authored-by: zanllp <qc@zanllp.cn>
Co-authored-by: Amour1688 <lcz_1996@foxmail.com>
2021-09-25 16:51:32 +08:00

591 lines
16 KiB
Vue

import LocaleReceiver from '../locale-provider/LocaleReceiver';
import warning from '../_util/warning';
import TransButton from '../_util/transButton';
import raf from '../_util/raf';
import { isStyleSupport } from '../_util/styleChecker';
import Editable from './Editable';
import measure from './util';
import PropTypes from '../_util/vue-types';
import type { TypographyProps } from './Typography';
import Typography from './Typography';
import ResizeObserver from '../vc-resize-observer';
import Tooltip from '../tooltip';
import copy from '../_util/copy-to-clipboard';
import CheckOutlined from '@ant-design/icons-vue/CheckOutlined';
import CopyOutlined from '@ant-design/icons-vue/CopyOutlined';
import EditOutlined from '@ant-design/icons-vue/EditOutlined';
import type { VNodeTypes, CSSProperties } from 'vue';
import {
defineComponent,
reactive,
ref,
onMounted,
onBeforeUnmount,
watch,
watchEffect,
nextTick,
computed,
toRaw,
} from 'vue';
import type { AutoSizeType } from '../input/ResizableTextArea';
import useConfigInject from '../_util/hooks/useConfigInject';
import type { EventHandler } from '../_util/EventInterface';
import omit from '../_util/omit';
export type BaseType = 'secondary' | 'success' | 'warning' | 'danger';
const isLineClampSupport = isStyleSupport('webkitLineClamp');
const isTextOverflowSupport = isStyleSupport('textOverflow');
export interface CopyConfig {
text?: string;
onCopy?: () => void;
tooltip?: boolean;
}
export interface EditConfig {
editing?: boolean;
tooltip?: boolean;
onStart?: () => void;
onChange?: (value: string) => void;
onCancel?: () => void;
onEnd?: () => void;
maxlength?: number;
autoSize?: boolean | AutoSizeType;
}
export interface EllipsisConfig {
rows?: number;
expandable?: boolean;
suffix?: string;
symbol?: string;
onExpand?: EventHandler;
onEllipsis?: (ellipsis: boolean) => void;
tooltip?: boolean;
}
export interface BlockProps extends TypographyProps {
title?: string;
editable?: boolean | EditConfig;
copyable?: boolean | CopyConfig;
type?: BaseType;
disabled?: boolean;
ellipsis?: boolean | EllipsisConfig;
// decorations
code?: boolean;
mark?: boolean;
underline?: boolean;
delete?: boolean;
strong?: boolean;
keyboard?: boolean;
content?: string;
}
interface Locale {
edit?: string;
copy?: string;
copied?: string;
expand?: string;
}
interface InternalBlockProps extends BlockProps {
component: string;
}
const ELLIPSIS_STR = '...';
const Base = defineComponent<InternalBlockProps>({
name: 'Base',
inheritAttrs: false,
emits: ['update:content'],
setup(props, { slots, attrs, emit }) {
const { prefixCls } = useConfigInject('typography', props);
const state = reactive({
edit: false,
copied: false,
ellipsisText: '',
ellipsisContent: null,
isEllipsis: false,
expanded: false,
clientRendered: false,
//locale
expandStr: '',
copyStr: '',
copiedStr: '',
editStr: '',
copyId: undefined,
rafId: undefined,
prevProps: undefined,
originContent: '',
});
const contentRef = ref();
const editIcon = ref();
const ellipsis = computed((): EllipsisConfig => {
const ellipsis = props.ellipsis;
if (!ellipsis) return {};
return {
rows: 1,
expandable: false,
...(typeof ellipsis === 'object' ? ellipsis : null),
};
});
onMounted(() => {
state.clientRendered = true;
});
onBeforeUnmount(() => {
window.clearTimeout(state.copyId);
raf.cancel(state.rafId);
});
watch(
[() => ellipsis.value.rows, () => props.content],
() => {
nextTick(() => {
resizeOnNextFrame();
});
},
{ flush: 'post', deep: true, immediate: true },
);
watchEffect(() => {
if (props.content === undefined) {
warning(
!props.editable,
'Typography',
'When `editable` is enabled, please use `content` instead of children',
);
warning(
!props.ellipsis,
'Typography',
'When `ellipsis` is enabled, please use `content` instead of children',
);
}
});
function getChildrenText(): string {
return props.ellipsis || props.editable ? props.content : contentRef.value?.$el?.innerText;
}
// =============== Expand ===============
function onExpandClick(e: MouseEvent) {
const { onExpand } = ellipsis.value;
state.expanded = true;
onExpand?.(e);
}
// ================ Edit ================
function onEditClick(e: MouseEvent) {
e.preventDefault();
state.originContent = props.content;
triggerEdit(true);
}
function onEditChange(value: string) {
onContentChange(value);
triggerEdit(false);
}
function onContentChange(value: string) {
const { onChange } = editable.value;
if (value !== props.content) {
emit('update:content', value);
onChange?.(value);
}
}
function onEditCancel() {
editable.value.onCancel?.();
triggerEdit(false);
}
// ================ Copy ================
function onCopyClick(e: MouseEvent) {
e.preventDefault();
const { copyable } = props;
const copyConfig = {
...(typeof copyable === 'object' ? copyable : null),
};
if (copyConfig.text === undefined) {
copyConfig.text = getChildrenText();
}
copy(copyConfig.text || '');
state.copied = true;
nextTick(() => {
if (copyConfig.onCopy) {
copyConfig.onCopy();
}
state.copyId = window.setTimeout(() => {
state.copied = false;
}, 3000);
});
}
const editable = computed(() => {
const editable = props.editable;
if (!editable) return { editing: state.edit };
return {
editing: state.edit,
...(typeof editable === 'object' ? editable : null),
};
});
function triggerEdit(edit: boolean) {
const { onStart } = editable.value;
if (edit && onStart) {
onStart();
}
state.edit = edit;
nextTick(() => {
if (!edit) {
editIcon.value?.focus();
}
});
}
// ============== Ellipsis ==============
function resizeOnNextFrame() {
raf.cancel(state.rafId);
state.rafId = raf(() => {
// Do not bind `syncEllipsis`. It need for test usage on prototype
syncEllipsis();
});
}
const canUseCSSEllipsis = computed(() => {
const { rows, expandable, suffix, onEllipsis, tooltip } = ellipsis.value;
if (suffix || tooltip) return false;
// Can't use css ellipsis since we need to provide the place for button
if (props.editable || props.copyable || expandable || onEllipsis) {
return false;
}
if (rows === 1) {
return isTextOverflowSupport;
}
return isLineClampSupport;
});
const syncEllipsis = () => {
const { ellipsisText, isEllipsis } = state;
const { rows, suffix, onEllipsis } = ellipsis.value;
if (
!rows ||
rows < 0 ||
!contentRef.value?.$el ||
state.expanded ||
props.content === undefined
)
return;
// Do not measure if css already support ellipsis
if (canUseCSSEllipsis.value) return;
const {
content,
text,
ellipsis: ell,
} = measure(
contentRef.value?.$el,
{ rows, suffix },
props.content,
renderOperations(true),
ELLIPSIS_STR,
);
if (ellipsisText !== text || state.isEllipsis !== ell) {
state.ellipsisText = text;
state.ellipsisContent = content;
state.isEllipsis = ell;
if (isEllipsis !== ell && onEllipsis) {
onEllipsis(ell);
}
}
};
function wrapperDecorations(
{ mark, code, underline, delete: del, strong, keyboard }: BlockProps,
content,
) {
let currentContent = content;
function wrap(needed: boolean, Tag: string) {
if (!needed) return;
currentContent = <Tag>{currentContent}</Tag>;
}
wrap(strong, 'strong');
wrap(underline, 'u');
wrap(del, 'del');
wrap(code, 'code');
wrap(mark, 'mark');
wrap(keyboard, 'kbd');
return currentContent;
}
function renderExpand(forceRender?: boolean) {
const { expandable, symbol } = ellipsis.value;
if (!expandable) return null;
// force render expand icon for measure usage or it will cause dead loop
if (!forceRender && (state.expanded || !state.isEllipsis)) return null;
const expandContent =
(slots.ellipsisSymbol ? slots.ellipsisSymbol() : symbol) || state.expandStr;
return (
<a
key="expand"
class={`${prefixCls.value}-expand`}
onClick={onExpandClick}
aria-label={state.expandStr}
>
{expandContent}
</a>
);
}
function renderEdit() {
if (!props.editable) return;
const { tooltip } = props.editable as EditConfig;
const icon = slots.editableIcon ? slots.editableIcon() : <EditOutlined role="button" />;
const title = slots.editableTooltip ? slots.editableTooltip() : state.editStr;
const ariaLabel = typeof title === 'string' ? title : '';
return (
<Tooltip key="edit" title={tooltip === false ? '' : title}>
<TransButton
ref={editIcon}
class={`${prefixCls.value}-edit`}
onClick={onEditClick}
aria-label={ariaLabel}
>
{icon}
</TransButton>
</Tooltip>
);
}
function renderCopy() {
if (!props.copyable) return;
const { tooltip } = props.copyable as CopyConfig;
const defaultTitle = state.copied ? state.copiedStr : state.copyStr;
const title = slots.copyableTooltip
? slots.copyableTooltip({ copied: state.copied })
: defaultTitle;
const ariaLabel = typeof title === 'string' ? title : '';
const defaultIcon = state.copied ? <CheckOutlined /> : <CopyOutlined />;
const icon = slots.copyableIcon
? slots.copyableIcon({ copied: !!state.copied })
: defaultIcon;
return (
<Tooltip key="copy" title={tooltip === false ? '' : title}>
<TransButton
class={[
`${prefixCls.value}-copy`,
{ [`${prefixCls.value}-copy-success`]: state.copied },
]}
onClick={onCopyClick}
aria-label={ariaLabel}
>
{icon}
</TransButton>
</Tooltip>
);
}
function renderEditInput() {
const { class: className, style } = attrs;
const { maxlength, autoSize, onEnd } = editable.value;
return (
<Editable
class={className}
style={style}
prefixCls={prefixCls.value}
value={props.content}
originContent={state.originContent}
maxlength={maxlength}
autoSize={autoSize}
onSave={onEditChange}
onChange={onContentChange}
onCancel={onEditCancel}
onEnd={onEnd}
/>
);
}
function renderOperations(forceRenderExpanded?: boolean) {
return [renderExpand(forceRenderExpanded), renderEdit(), renderCopy()].filter(node => node);
}
return () => {
const { editing } = editable.value;
const children =
props.ellipsis || props.editable
? props.content !== undefined
? props.content
: slots.default?.()
: slots.default
? slots.default()
: props.content;
if (editing) {
return renderEditInput();
}
return (
<LocaleReceiver
componentName="Text"
children={(locale: Locale) => {
const {
type,
disabled,
content,
class: className,
style,
...restProps
} = {
...props,
...attrs,
};
const { rows, suffix, tooltip } = ellipsis.value;
const { edit, copy: copyStr, copied, expand } = locale;
state.editStr = edit;
state.copyStr = copyStr;
state.copiedStr = copied;
state.expandStr = expand;
const textProps = omit(restProps, [
'prefixCls',
'editable',
'copyable',
'ellipsis',
'mark',
'code',
'delete',
'underline',
'strong',
'keyboard',
]);
const cssEllipsis = canUseCSSEllipsis.value;
const cssTextOverflow = rows === 1 && cssEllipsis;
const cssLineClamp = rows && rows > 1 && cssEllipsis;
let textNode = children as VNodeTypes;
let ariaLabel: string | undefined;
// Only use js ellipsis when css ellipsis not support
if (rows && state.isEllipsis && !state.expanded && !cssEllipsis) {
const { title } = restProps;
let restContent = title || '';
if (!title && (typeof children === 'string' || typeof children === 'number')) {
restContent = String(children);
}
// show rest content as title on symbol
restContent = restContent?.slice(String(state.ellipsisContent || '').length);
// We move full content to outer element to avoid repeat read the content by accessibility
textNode = (
<>
{toRaw(state.ellipsisContent)}
<span title={restContent} aria-hidden="true">
{ELLIPSIS_STR}
</span>
{suffix}
</>
);
} else {
textNode = (
<>
{children}
{suffix}
</>
);
}
textNode = wrapperDecorations(props, textNode);
const showTooltip =
tooltip && rows && state.isEllipsis && !state.expanded && !cssEllipsis;
const title = slots.ellipsisTooltip ? slots.ellipsisTooltip() : tooltip;
return (
<ResizeObserver onResize={resizeOnNextFrame} disabled={!rows}>
<Typography
ref={contentRef}
class={[
{
[`${prefixCls.value}-${type}`]: type,
[`${prefixCls.value}-disabled`]: disabled,
[`${prefixCls.value}-ellipsis`]: rows,
[`${prefixCls.value}-single-line`]: rows === 1,
[`${prefixCls.value}-ellipsis-single-line`]: cssTextOverflow,
[`${prefixCls.value}-ellipsis-multiple-line`]: cssLineClamp,
},
className,
]}
style={{
...(style as CSSProperties),
WebkitLineClamp: cssLineClamp ? rows : undefined,
}}
aria-label={ariaLabel}
{...textProps}
>
{showTooltip ? (
<Tooltip title={tooltip === true ? children : title}>
<span>{textNode}</span>
</Tooltip>
) : (
textNode
)}
{renderOperations()}
</Typography>
</ResizeObserver>
);
}}
/>
);
};
},
});
export const baseProps = () => ({
editable: PropTypes.oneOfType([PropTypes.looseBool, PropTypes.object]),
copyable: PropTypes.oneOfType([PropTypes.looseBool, PropTypes.object]),
prefixCls: PropTypes.string,
component: PropTypes.string,
type: PropTypes.oneOf(['secondary', 'success', 'danger', 'warning']),
disabled: PropTypes.looseBool,
ellipsis: PropTypes.oneOfType([PropTypes.looseBool, PropTypes.object]),
code: PropTypes.looseBool,
mark: PropTypes.looseBool,
underline: PropTypes.looseBool,
delete: PropTypes.looseBool,
strong: PropTypes.looseBool,
keyboard: PropTypes.looseBool,
content: PropTypes.string,
});
Base.props = baseProps();
export default Base;