feat: add typography (#3807)
* feat: add typography * fix: review typography * fix: review typography * feat: update typography * feat: update typography * fix: typography * test: update typography Co-authored-by: zkwolf <chenhao5866@gmail.com>pull/3771/head
parent
21bf0a3a40
commit
b0025d9e79
|
@ -36,7 +36,8 @@
|
|||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{ "vars": "all", "args": "after-used", "ignoreRestSiblings": true }
|
||||
]
|
||||
],
|
||||
"@typescript-eslint/ban-ts-comment": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
import deselectCurrent from './toggle-selection';
|
||||
|
||||
interface Options {
|
||||
debug?: boolean;
|
||||
message?: string;
|
||||
format?: string; // MIME type
|
||||
onCopy?: (clipboardData: object) => void;
|
||||
}
|
||||
|
||||
const clipboardToIE11Formatting = {
|
||||
'text/plain': 'Text',
|
||||
'text/html': 'Url',
|
||||
default: 'Text',
|
||||
};
|
||||
|
||||
const defaultMessage = 'Copy to clipboard: #{key}, Enter';
|
||||
|
||||
function format(message: string) {
|
||||
const copyKey = (/mac os x/i.test(navigator.userAgent) ? '⌘' : 'Ctrl') + '+C';
|
||||
return message.replace(/#{\s*key\s*}/g, copyKey);
|
||||
}
|
||||
|
||||
function copy(text: string, options?: Options): boolean {
|
||||
let message,
|
||||
reselectPrevious,
|
||||
range,
|
||||
selection,
|
||||
mark,
|
||||
success = false;
|
||||
if (!options) {
|
||||
options = {};
|
||||
}
|
||||
const debug = options.debug || false;
|
||||
try {
|
||||
reselectPrevious = deselectCurrent();
|
||||
|
||||
range = document.createRange();
|
||||
selection = document.getSelection();
|
||||
|
||||
mark = document.createElement('span');
|
||||
mark.textContent = text;
|
||||
// reset user styles for span element
|
||||
mark.style.all = 'unset';
|
||||
// prevents scrolling to the end of the page
|
||||
mark.style.position = 'fixed';
|
||||
mark.style.top = 0;
|
||||
mark.style.clip = 'rect(0, 0, 0, 0)';
|
||||
// used to preserve spaces and line breaks
|
||||
mark.style.whiteSpace = 'pre';
|
||||
// do not inherit user-select (it may be `none`)
|
||||
mark.style.webkitUserSelect = 'text';
|
||||
mark.style.MozUserSelect = 'text';
|
||||
mark.style.msUserSelect = 'text';
|
||||
mark.style.userSelect = 'text';
|
||||
mark.addEventListener('copy', function(e) {
|
||||
e.stopPropagation();
|
||||
if (options.format) {
|
||||
e.preventDefault();
|
||||
if (typeof e.clipboardData === 'undefined') {
|
||||
// IE 11
|
||||
debug && console.warn('unable to use e.clipboardData');
|
||||
debug && console.warn('trying IE specific stuff');
|
||||
(window as any).clipboardData.clearData();
|
||||
const format =
|
||||
clipboardToIE11Formatting[options.format] || clipboardToIE11Formatting['default'];
|
||||
(window as any).clipboardData.setData(format, text);
|
||||
} else {
|
||||
// all other browsers
|
||||
e.clipboardData.clearData();
|
||||
e.clipboardData.setData(options.format, text);
|
||||
}
|
||||
}
|
||||
if (options.onCopy) {
|
||||
e.preventDefault();
|
||||
options.onCopy(e.clipboardData);
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(mark);
|
||||
|
||||
range.selectNodeContents(mark);
|
||||
selection.addRange(range);
|
||||
|
||||
const successful = document.execCommand('copy');
|
||||
if (!successful) {
|
||||
throw new Error('copy command was unsuccessful');
|
||||
}
|
||||
success = true;
|
||||
} catch (err) {
|
||||
debug && console.error('unable to copy using execCommand: ', err);
|
||||
debug && console.warn('trying IE specific stuff');
|
||||
try {
|
||||
(window as any).clipboardData.setData(options.format || 'text', text);
|
||||
options.onCopy && options.onCopy((window as any).clipboardData);
|
||||
success = true;
|
||||
} catch (err) {
|
||||
debug && console.error('unable to copy using clipboardData: ', err);
|
||||
debug && console.error('falling back to prompt');
|
||||
message = format('message' in options ? options.message : defaultMessage);
|
||||
window.prompt(message, text);
|
||||
}
|
||||
} finally {
|
||||
if (selection) {
|
||||
if (typeof selection.removeRange == 'function') {
|
||||
selection.removeRange(range);
|
||||
} else {
|
||||
selection.removeAllRanges();
|
||||
}
|
||||
}
|
||||
|
||||
if (mark) {
|
||||
document.body.removeChild(mark);
|
||||
}
|
||||
reselectPrevious();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
export default copy;
|
|
@ -0,0 +1,41 @@
|
|||
// copy from https://github.com/sudodoki/toggle-selection
|
||||
// refactor to esm
|
||||
const deselectCurrent = (): (() => void) => {
|
||||
const selection = document.getSelection();
|
||||
if (!selection.rangeCount) {
|
||||
return function() {};
|
||||
}
|
||||
let active = document.activeElement as any;
|
||||
|
||||
const ranges = [];
|
||||
for (let i = 0; i < selection.rangeCount; i++) {
|
||||
ranges.push(selection.getRangeAt(i));
|
||||
}
|
||||
|
||||
switch (
|
||||
active.tagName.toUpperCase() // .toUpperCase handles XHTML
|
||||
) {
|
||||
case 'INPUT':
|
||||
case 'TEXTAREA':
|
||||
active.blur();
|
||||
break;
|
||||
|
||||
default:
|
||||
active = null;
|
||||
break;
|
||||
}
|
||||
|
||||
selection.removeAllRanges();
|
||||
return function() {
|
||||
selection.type === 'Caret' && selection.removeAllRanges();
|
||||
|
||||
if (!selection.rangeCount) {
|
||||
ranges.forEach(function(range) {
|
||||
selection.addRange(range);
|
||||
});
|
||||
}
|
||||
|
||||
active && active.focus();
|
||||
};
|
||||
};
|
||||
export default deselectCurrent;
|
|
@ -0,0 +1,8 @@
|
|||
import { computed, inject } from 'vue';
|
||||
import { defaultConfigProvider } from '../../config-provider';
|
||||
|
||||
export default (name: string, props: Record<any, any>) => {
|
||||
const configProvider = inject('configProvider', defaultConfigProvider);
|
||||
const prefixCls = computed(() => configProvider.getPrefixCls(name, props.prefixCls));
|
||||
return { configProvider, prefixCls };
|
||||
};
|
|
@ -146,6 +146,8 @@ import { default as Descriptions } from './descriptions';
|
|||
import { default as PageHeader } from './page-header';
|
||||
import { default as Space } from './space';
|
||||
|
||||
import { default as Typography } from './typography';
|
||||
|
||||
const components = [
|
||||
Affix,
|
||||
Anchor,
|
||||
|
@ -210,6 +212,7 @@ const components = [
|
|||
PageHeader,
|
||||
Space,
|
||||
Image,
|
||||
Typography,
|
||||
];
|
||||
|
||||
const install = function(app: App) {
|
||||
|
@ -298,6 +301,7 @@ export {
|
|||
PageHeader,
|
||||
Space,
|
||||
Image,
|
||||
Typography,
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
@ -13,6 +13,8 @@ const TextAreaProps = {
|
|||
autosize: withUndefined(PropTypes.oneOfType([Object, Boolean])),
|
||||
autoSize: withUndefined(PropTypes.oneOfType([Object, Boolean])),
|
||||
showCount: PropTypes.looseBool,
|
||||
onCompositionstart: PropTypes.func,
|
||||
onCompositionend: PropTypes.func,
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
|
|
|
@ -155,5 +155,6 @@ export default function calculateNodeHeight(
|
|||
minHeight: `${minHeight}px`,
|
||||
maxHeight: `${maxHeight}px`,
|
||||
overflowY,
|
||||
resize: 'none',
|
||||
};
|
||||
}
|
||||
|
|
|
@ -61,4 +61,5 @@ import './page-header/style';
|
|||
import './form/style';
|
||||
import './space/style';
|
||||
import './image/style';
|
||||
import './typography/style';
|
||||
// import './color-picker/style';
|
||||
|
|
|
@ -47,3 +47,12 @@
|
|||
@typography-title-margin-bottom
|
||||
);
|
||||
}
|
||||
.typography-title-5() {
|
||||
.typography-title(
|
||||
@heading-5-size,
|
||||
@typography-title-font-weight,
|
||||
1.5,
|
||||
@heading-color,
|
||||
@typography-title-margin-bottom
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,583 @@
|
|||
import omit from 'omit.js';
|
||||
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 Typography, { TypographyProps } 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 {
|
||||
defineComponent,
|
||||
VNodeTypes,
|
||||
VNode,
|
||||
reactive,
|
||||
ref,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
watch,
|
||||
watchEffect,
|
||||
nextTick,
|
||||
CSSProperties,
|
||||
computed,
|
||||
toRaw,
|
||||
} from 'vue';
|
||||
import { AutoSizeType } from '../input/ResizableTextArea';
|
||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||
|
||||
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?: EventHandlerNonNull;
|
||||
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 (!('content' in props)) {
|
||||
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 saveTypographyRef(node: VNode) {
|
||||
contentRef.value = node;
|
||||
}
|
||||
|
||||
function saveEditIconRef(node: VNode) {
|
||||
editIcon.value = node;
|
||||
}
|
||||
|
||||
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() {
|
||||
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) {
|
||||
onChange?.(value);
|
||||
emit('update:content', value);
|
||||
}
|
||||
}
|
||||
|
||||
function onEditCancel() {
|
||||
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={saveEditIconRef}
|
||||
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 } = 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderOperations(forceRenderExpanded?: boolean) {
|
||||
return [renderExpand(forceRenderExpanded), renderEdit(), renderCopy()].filter(node => node);
|
||||
}
|
||||
|
||||
return () => {
|
||||
const { editing } = editable.value;
|
||||
const children =
|
||||
props.ellipsis || props.editable
|
||||
? 'content' in props
|
||||
? 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={saveTypographyRef}
|
||||
class={[
|
||||
{ [`${prefixCls.value}-${type}`]: type },
|
||||
{ [`${prefixCls.value}-disabled`]: disabled },
|
||||
{ [`${prefixCls.value}-ellipsis`]: rows },
|
||||
{ [`${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;
|
|
@ -0,0 +1,124 @@
|
|||
import KeyCode from '../_util/KeyCode';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import TextArea from '../input/TextArea';
|
||||
import EnterOutlined from '@ant-design/icons-vue/EnterOutlined';
|
||||
import { defineComponent, ref, reactive, watch, onMounted } from 'vue';
|
||||
|
||||
const Editable = defineComponent({
|
||||
props: {
|
||||
prefixCls: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
maxlength: PropTypes.number,
|
||||
autoSize: PropTypes.oneOfType([PropTypes.looseBool, PropTypes.object]),
|
||||
onSave: PropTypes.func,
|
||||
onCancel: PropTypes.func,
|
||||
onEnd: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
originContent: PropTypes.string,
|
||||
},
|
||||
emits: ['save', 'cancel', 'end', 'change'],
|
||||
setup(props, { emit }) {
|
||||
const state = reactive({
|
||||
current: props.value || '',
|
||||
lastKeyCode: undefined,
|
||||
inComposition: false,
|
||||
cancelFlag: false,
|
||||
});
|
||||
watch(
|
||||
() => props.value,
|
||||
current => {
|
||||
state.current = current;
|
||||
},
|
||||
);
|
||||
|
||||
const textArea = ref();
|
||||
|
||||
onMounted(() => {
|
||||
if (textArea.value) {
|
||||
const resizableTextArea = textArea.value?.resizableTextArea;
|
||||
const innerTextArea = resizableTextArea?.textArea;
|
||||
innerTextArea.focus();
|
||||
const { length } = innerTextArea.value;
|
||||
innerTextArea.setSelectionRange(length, length);
|
||||
}
|
||||
});
|
||||
|
||||
function saveTextAreaRef(node: any) {
|
||||
textArea.value = node;
|
||||
}
|
||||
|
||||
function onChange({ target: { value } }) {
|
||||
state.current = value.replace(/[\r\n]/g, '');
|
||||
emit('change', state.current);
|
||||
}
|
||||
|
||||
function onCompositionStart() {
|
||||
state.inComposition = true;
|
||||
}
|
||||
|
||||
function onCompositionEnd() {
|
||||
state.inComposition = false;
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
const { keyCode } = e;
|
||||
if (keyCode === KeyCode.ENTER) {
|
||||
e.preventDefault();
|
||||
}
|
||||
// We don't record keyCode when IME is using
|
||||
if (state.inComposition) return;
|
||||
|
||||
state.lastKeyCode = keyCode;
|
||||
}
|
||||
|
||||
function onKeyUp(e: KeyboardEvent) {
|
||||
const { keyCode, ctrlKey, altKey, metaKey, shiftKey } = e;
|
||||
|
||||
// Check if it's a real key
|
||||
if (
|
||||
state.lastKeyCode === keyCode &&
|
||||
!state.inComposition &&
|
||||
!ctrlKey &&
|
||||
!altKey &&
|
||||
!metaKey &&
|
||||
!shiftKey
|
||||
) {
|
||||
if (keyCode === KeyCode.ENTER) {
|
||||
confirmChange();
|
||||
emit('end');
|
||||
} else if (keyCode === KeyCode.ESC) {
|
||||
state.current = props.originContent;
|
||||
emit('cancel');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onBlur() {
|
||||
confirmChange();
|
||||
}
|
||||
|
||||
function confirmChange() {
|
||||
emit('save', state.current.trim());
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div class={`${props.prefixCls} ${props.prefixCls}-edit-content`}>
|
||||
<TextArea
|
||||
ref={saveTextAreaRef}
|
||||
maxlength={props.maxlength}
|
||||
value={state.current}
|
||||
onChange={onChange}
|
||||
onKeydown={onKeyDown}
|
||||
onKeyup={onKeyUp}
|
||||
onCompositionstart={onCompositionStart}
|
||||
onCompositionend={onCompositionEnd}
|
||||
onBlur={onBlur}
|
||||
autoSize={props.autoSize === undefined || props.autoSize}
|
||||
/>
|
||||
<EnterOutlined class={`${props.prefixCls}-edit-content-confirm`} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default Editable;
|
|
@ -0,0 +1,35 @@
|
|||
import { AnchorHTMLAttributes, FunctionalComponent } from 'vue';
|
||||
import warning from '../_util/warning';
|
||||
import Base, { baseProps, BlockProps } from './Base';
|
||||
import Omit from 'omit.js';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
|
||||
export interface LinkProps extends BlockProps, Omit<AnchorHTMLAttributes, 'type'> {
|
||||
ellipsis?: boolean;
|
||||
}
|
||||
|
||||
const Link: FunctionalComponent<LinkProps> = (props, { slots, attrs }) => {
|
||||
const { ellipsis, rel, ...restProps } = { ...props, ...attrs };
|
||||
warning(
|
||||
typeof ellipsis !== 'object',
|
||||
'Typography.Link',
|
||||
'`ellipsis` only supports boolean value.',
|
||||
);
|
||||
const mergedProps = {
|
||||
...restProps,
|
||||
rel: rel === undefined && restProps.target === '_blank' ? 'noopener noreferrer' : rel,
|
||||
ellipsis: !!ellipsis,
|
||||
component: 'a',
|
||||
};
|
||||
// https://github.com/ant-design/ant-design/issues/26622
|
||||
// @ts-ignore
|
||||
delete mergedProps.navigate;
|
||||
|
||||
return <Base {...mergedProps} v-slots={slots}></Base>;
|
||||
};
|
||||
|
||||
Link.displayName = 'ATypographyLink';
|
||||
Link.inheritAttrs = false;
|
||||
Link.props = Omit({ ...baseProps(), ellipsis: PropTypes.looseBool }, ['component']);
|
||||
|
||||
export default Link;
|
|
@ -0,0 +1,19 @@
|
|||
import Omit from 'omit.js';
|
||||
import { FunctionalComponent } from 'vue';
|
||||
import Base, { BlockProps, baseProps } from './Base';
|
||||
|
||||
const Paragraph: FunctionalComponent<BlockProps> = (props, { slots, attrs }) => {
|
||||
const paragraphProps = {
|
||||
...props,
|
||||
component: 'div',
|
||||
...attrs,
|
||||
};
|
||||
|
||||
return <Base {...paragraphProps} v-slots={slots}></Base>;
|
||||
};
|
||||
|
||||
Paragraph.displayName = 'ATypographyParagraph';
|
||||
Paragraph.inheritAttrs = false;
|
||||
Paragraph.props = Omit(baseProps(), ['component']);
|
||||
|
||||
export default Paragraph;
|
|
@ -0,0 +1,35 @@
|
|||
import { FunctionalComponent } from 'vue';
|
||||
import warning from '../_util/warning';
|
||||
import Base, { baseProps, BlockProps, EllipsisConfig } from './Base';
|
||||
import Omit from 'omit.js';
|
||||
|
||||
export interface TextProps extends BlockProps {
|
||||
ellipsis?: boolean | Omit<EllipsisConfig, 'expandable' | 'rows' | 'onExpand'>;
|
||||
}
|
||||
|
||||
const Text: FunctionalComponent<TextProps> = (props, { slots, attrs }) => {
|
||||
const { ellipsis } = props;
|
||||
warning(
|
||||
typeof ellipsis !== 'object' ||
|
||||
!ellipsis ||
|
||||
(!('expandable' in ellipsis) && !('rows' in ellipsis)),
|
||||
'Typography.Text',
|
||||
'`ellipsis` do not support `expandable` or `rows` props.',
|
||||
);
|
||||
const textProps = {
|
||||
...props,
|
||||
ellipsis:
|
||||
ellipsis && typeof ellipsis === 'object'
|
||||
? Omit(ellipsis as any, ['expandable', 'rows'])
|
||||
: ellipsis,
|
||||
component: 'span',
|
||||
...attrs,
|
||||
};
|
||||
return <Base {...textProps} v-slots={slots}></Base>;
|
||||
};
|
||||
|
||||
Text.displayName = 'ATypographyText';
|
||||
Text.inheritAttrs = false;
|
||||
Text.props = Omit(baseProps(), ['component']);
|
||||
|
||||
export default Text;
|
|
@ -0,0 +1,35 @@
|
|||
import Omit from 'omit.js';
|
||||
import { FunctionalComponent } from 'vue';
|
||||
import { tupleNum } from '../_util/type';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import warning from '../_util/warning';
|
||||
import Base, { baseProps, BlockProps } from './Base';
|
||||
|
||||
const TITLE_ELE_LIST = tupleNum(1, 2, 3, 4, 5);
|
||||
|
||||
export type TitleProps = Omit<BlockProps & { level?: typeof TITLE_ELE_LIST[number] }, 'strong'>;
|
||||
|
||||
const Title: FunctionalComponent<TitleProps> = (props, { slots, attrs }) => {
|
||||
const { level = 1, ...restProps } = props;
|
||||
let component: string;
|
||||
if (TITLE_ELE_LIST.indexOf(level) !== -1) {
|
||||
component = `h${level}`;
|
||||
} else {
|
||||
warning(false, 'Typography', 'Title only accept `1 | 2 | 3 | 4 | 5` as `level` value.');
|
||||
component = 'h1';
|
||||
}
|
||||
|
||||
const titleProps = {
|
||||
...restProps,
|
||||
component,
|
||||
...attrs,
|
||||
};
|
||||
|
||||
return <Base {...titleProps} v-slots={slots}></Base>;
|
||||
};
|
||||
|
||||
Title.displayName = 'ATypographyTitle';
|
||||
Title.inheritAttrs = false;
|
||||
Title.props = Omit({ ...baseProps(), level: PropTypes.number }, ['component', 'strong']);
|
||||
|
||||
export default Title;
|
|
@ -0,0 +1,66 @@
|
|||
import Text from './Text';
|
||||
import Title from './Title';
|
||||
import Paragraph from './Paragraph';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import { defineComponent, HTMLAttributes, App, Plugin } from 'vue';
|
||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||
import Link from './Link';
|
||||
import Base from './Base';
|
||||
import classNames from '../_util/classNames';
|
||||
|
||||
export interface TypographyProps extends HTMLAttributes {
|
||||
prefixCls?: string;
|
||||
}
|
||||
|
||||
interface InternalTypographyProps extends TypographyProps {
|
||||
component?: string;
|
||||
}
|
||||
|
||||
const Typography = defineComponent<InternalTypographyProps>({
|
||||
name: 'ATypography',
|
||||
Base,
|
||||
Text,
|
||||
Title,
|
||||
Paragraph,
|
||||
Link,
|
||||
inheritAttrs: false,
|
||||
setup(props, { slots, attrs }) {
|
||||
const { prefixCls } = useConfigInject('typography', props);
|
||||
return () => {
|
||||
const {
|
||||
prefixCls: _prefixCls,
|
||||
class: _className,
|
||||
component: Component = 'article' as any,
|
||||
...restProps
|
||||
} = { ...props, ...attrs };
|
||||
return (
|
||||
<Component class={classNames(prefixCls.value, attrs.class)} {...restProps}>
|
||||
{slots.default?.()}
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Typography.props = {
|
||||
prefixCls: PropTypes.string,
|
||||
component: PropTypes.string,
|
||||
};
|
||||
|
||||
Typography.install = function(app: App) {
|
||||
app.component(Typography.name, Typography);
|
||||
app.component(Typography.Text.displayName, Text);
|
||||
app.component(Typography.Title.displayName, Title);
|
||||
app.component(Typography.Paragraph.displayName, Paragraph);
|
||||
app.component(Typography.Link.displayName, Link);
|
||||
return app;
|
||||
};
|
||||
|
||||
export default Typography as typeof Typography &
|
||||
Plugin & {
|
||||
readonly Text: typeof Text;
|
||||
readonly Title: typeof Title;
|
||||
readonly Paragraph: typeof Paragraph;
|
||||
readonly Link: typeof Link;
|
||||
readonly Base: typeof Base;
|
||||
};
|
|
@ -0,0 +1,101 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders ./antdv-demo/docs/typography/demo/base.md correctly 1`] = `
|
||||
<article class="ant-typography">
|
||||
<h1 class="ant-typography">Introduction</h1>
|
||||
<div class="ant-typography">
|
||||
In the process of internal desktop applications development, many different design specs and implementations would be involved, which might cause designers and developers difficulties and duplication and reduce the efficiency of development.
|
||||
</div>
|
||||
<div class="ant-typography">
|
||||
After massive project practice and summaries, Ant Design, a design language for background applications, is refined by Ant UED Team, which aims to <span class="ant-typography"><strong>uniform the user interface specs for internal background projects, lower the unnecessary cost of design differences and implementation and liberate the resources of design and front-end development</strong></span>.
|
||||
</div>
|
||||
<h2 class="ant-typography">Guidelines and Resources</h2>
|
||||
<div class="ant-typography">
|
||||
We supply a series of design principles, practical patterns and high quality design resources (<span class="ant-typography"><code>Sketch</code></span> and <span class="ant-typography"><code>Axure</code></span>), to help people create their product prototypes beautifully and efficiently.
|
||||
</div>
|
||||
<div class="ant-typography">
|
||||
<ul>
|
||||
<li><a href="/docs/spec/proximity">Principles</a></li>
|
||||
<li><a href="/docs/pattern/navigation">Patterns</a></li>
|
||||
<li><a href="/docs/resource/download">Resource Download</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div role="separator" class="ant-divider ant-divider-horizontal"></div>
|
||||
<h1 class="ant-typography">介绍</h1>
|
||||
<div class="ant-typography">
|
||||
蚂蚁的企业级产品是一个庞大且复杂的体系。这类产品不仅量级巨大且功能复杂,而且变动和并发频繁,常常需要设计与开发能够快速的做出响应。同时这类产品中有存在很多类似的页面以及组件,可以通过抽象得到一些稳定且高复用性的内容。
|
||||
</div>
|
||||
<div class="ant-typography">
|
||||
随着商业化的趋势,越来越多的企业级产品对更好的用户体验有了进一步的要求。带着这样的一个终极目标,我们(蚂蚁金服体验技术部)经过大量的项目实践和总结,逐步打磨出一个服务于企业级产品的设计体系 Ant Design。基于<span class="ant-typography"><mark>『确定』和『自然』</mark></span>的设计价值观,通过模块化的解决方案,降低冗余的生产成本,让设计者专注于<span class="ant-typography"><strong>更好的用户体验</strong></span>。
|
||||
</div>
|
||||
<h2 class="ant-typography">设计资源</h2>
|
||||
<div class="ant-typography">
|
||||
我们提供完善的设计原则、最佳实践和设计资源文件(<span class="ant-typography"><code>Sketch</code></span> 和 <span class="ant-typography"><code>Axure</code></span>),来帮助业务快速设计出高质量的产品原型。
|
||||
</div>
|
||||
<div class="ant-typography">
|
||||
<ul>
|
||||
<li><a href="/docs/spec/proximity">设计原则</a></li>
|
||||
<li><a href="/docs/pattern/navigation">设计模式</a></li>
|
||||
<li><a href="/docs/resource/download">设计资源</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
|
||||
exports[`renders ./antdv-demo/docs/typography/demo/ellipsis.md correctly 1`] = `
|
||||
<div>
|
||||
<div class="ant-typography ant-typography-ellipsis ant-typography-ellipsis-single-line">
|
||||
Ant Design, a design language for background applications, is refined by Ant UED Team. Ant
|
||||
Design, a design language for background applications, is refined by Ant UED Team. Ant Design,
|
||||
a design language for background applications, is refined by Ant UED Team. Ant Design, a
|
||||
design language for background applications, is refined by Ant UED Team. Ant Design, a design
|
||||
language for background applications, is refined by Ant UED Team. Ant Design, a design
|
||||
language for background applications, is refined by Ant UED Team.
|
||||
</div>
|
||||
<div class="ant-typography ant-typography-ellipsis">
|
||||
Ant Design, a design language for background applications, is refined by Ant UED Team. Ant
|
||||
Design, a design language for background applications, is refined by Ant UED Team. Ant Design,
|
||||
a design language for background applications, is refined by Ant UED Team. Ant Design, a
|
||||
design language for background applications, is refined by Ant UED Team. Ant Design, a design
|
||||
language for background applications, is refined by Ant UED Team. Ant Design, a design
|
||||
language for background applications, is refined by Ant UED Team.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./antdv-demo/docs/typography/demo/interactive.md correctly 1`] = `
|
||||
<div>
|
||||
<div class="ant-typography">This is an editable text.<div role="button" tabindex="0" style="padding: 0px; line-height: inherit; display: inline-block; border: 0px; background: transparent;" aria-label="Edit" class="ant-typography-edit"><span role="button" aria-label="edit" class="anticon anticon-edit"><svg viewBox="64 64 896 896" focusable="false" data-icon="edit" width="1em" height="1em" fill="currentColor" aria-hidden="true" class=""><path d="M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z"></path></svg></span></div>
|
||||
</div>
|
||||
<div class="ant-typography">This is a copyable text.<div role="button" tabindex="0" style="padding: 0px; line-height: inherit; display: inline-block; border: 0px; background: transparent;" aria-label="Copy" class="ant-typography-copy"><span role="img" aria-label="copy" class="anticon anticon-copy"><svg viewBox="64 64 896 896" focusable="false" data-icon="copy" width="1em" height="1em" fill="currentColor" aria-hidden="true" class=""><path d="M832 64H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496v688c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V96c0-17.7-14.3-32-32-32zM704 192H192c-17.7 0-32 14.3-32 32v530.7c0 8.5 3.4 16.6 9.4 22.6l173.3 173.3c2.2 2.2 4.7 4 7.4 5.5v1.9h4.2c3.5 1.3 7.2 2 11 2H704c17.7 0 32-14.3 32-32V224c0-17.7-14.3-32-32-32zM350 856.2L263.9 770H350v86.2zM664 888H414V746c0-22.1-17.9-40-40-40H232V264h432v624z"></path></svg></span></div>
|
||||
</div>
|
||||
<div class="ant-typography">Replace copy text.<div role="button" tabindex="0" style="padding: 0px; line-height: inherit; display: inline-block; border: 0px; background: transparent;" aria-label="Copy" class="ant-typography-copy"><span role="img" aria-label="copy" class="anticon anticon-copy"><svg viewBox="64 64 896 896" focusable="false" data-icon="copy" width="1em" height="1em" fill="currentColor" aria-hidden="true" class=""><path d="M832 64H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496v688c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V96c0-17.7-14.3-32-32-32zM704 192H192c-17.7 0-32 14.3-32 32v530.7c0 8.5 3.4 16.6 9.4 22.6l173.3 173.3c2.2 2.2 4.7 4 7.4 5.5v1.9h4.2c3.5 1.3 7.2 2 11 2H704c17.7 0 32-14.3 32-32V224c0-17.7-14.3-32-32-32zM350 856.2L263.9 770H350v86.2zM664 888H414V746c0-22.1-17.9-40-40-40H232V264h432v624z"></path></svg></span></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./antdv-demo/docs/typography/demo/suffix.md correctly 1`] = `
|
||||
<div>
|
||||
<div tabindex="-1" class="ant-slider">
|
||||
<div class="ant-slider-rail"></div>
|
||||
<div class="ant-slider-track" style="left: 0%; width: 0%;"></div>
|
||||
<div class="ant-slider-step"></div>
|
||||
<div role="slider" tabindex="0" aria-valuemin="1" aria-valuemax="10" aria-valuenow="1" class="ant-slider-handle" style="left: 0%; transform: translateX(-50%);"></div>
|
||||
<div class="ant-slider-mark"></div>
|
||||
</div>
|
||||
<div class="ant-typography ant-typography-ellipsis" title="To be, or not to be, that is a question: Whether it is nobler in the mind to suffer. The slings and arrows of outrageous fortune Or to take arms against a sea of troubles, And by opposing end them? To die: to sleep; No more; and by a sleep to say we end The heart-ache and the thousand natural shocks That flesh is heir to, 'tis a consummation Devoutly to be wish'd. To die, to sleep To sleep- perchance to dream: ay, there's the rub! For in that sleep of death what dreams may come When we have shuffled off this mortal coil, Must give us pause. There 's the respect That makes calamity of so long life--William Shakespeare">
|
||||
To be, or not to be, that is a question: Whether it is nobler in the mind to suffer. The slings and arrows of outrageous fortune Or to take arms against a sea of troubles, And by opposing end them? To die: to sleep; No more; and by a sleep to say we end The heart-ache and the thousand natural shocks That flesh is heir to, 'tis a consummation Devoutly to be wish'd. To die, to sleep To sleep- perchance to dream: ay, there's the rub! For in that sleep of death what dreams may come When we have shuffled off this mortal coil, Must give us pause. There 's the respect That makes calamity of so long life
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./antdv-demo/docs/typography/demo/text.md correctly 1`] = `<div><span class="ant-typography">Ant Design</span> <br> <span class="ant-typography ant-typography-secondary">Ant Design</span> <br> <span class="ant-typography ant-typography-warning">Ant Design</span> <br> <span class="ant-typography ant-typography-danger">Ant Design</span> <br> <span class="ant-typography ant-typography-disabled">Ant Design</span> <br> <span class="ant-typography"><mark>Ant Design</mark></span> <br> <span class="ant-typography"><code>Ant Design</code></span> <br> <span class="ant-typography"><u>Ant Design</u></span> <br> <span class="ant-typography"><del>Ant Design</del></span> <br> <span class="ant-typography"><strong>Ant Design</strong></span></div>`;
|
||||
|
||||
exports[`renders ./antdv-demo/docs/typography/demo/title.md correctly 1`] = `
|
||||
<div>
|
||||
<h1 class="ant-typography">h1. Ant Design</h1>
|
||||
<h2 class="ant-typography">h2. Ant Design</h2>
|
||||
<h3 class="ant-typography">h3. Ant Design</h3>
|
||||
<h4 class="ant-typography">h4. Ant Design</h4>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,3 @@
|
|||
import demoTest from '../../../tests/shared/demoTest';
|
||||
|
||||
demoTest('typography');
|
|
@ -0,0 +1,350 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import { asyncExpect, sleep } from '@/tests/utils';
|
||||
import KeyCode from '../../_util/KeyCode';
|
||||
import copy from '../../_util/copy-to-clipboard';
|
||||
import Typography from '../Typography';
|
||||
import Title from '../Title';
|
||||
import Paragraph from '../Paragraph';
|
||||
import Link from '../Link';
|
||||
import mountTest from '../../../tests/shared/mountTest';
|
||||
import { nextTick, createTextVNode } from 'vue';
|
||||
|
||||
const Base = Typography.Base;
|
||||
describe('Typography', () => {
|
||||
mountTest(Paragraph);
|
||||
mountTest(Base);
|
||||
mountTest(Title);
|
||||
mountTest(Link);
|
||||
|
||||
const LINE_STR_COUNT = 20;
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
// Mock offsetHeight
|
||||
const originOffsetHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetHeight')
|
||||
.get;
|
||||
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
|
||||
get() {
|
||||
let html = this.innerHTML;
|
||||
html = html.replace(/<[^>]*>/g, '');
|
||||
const lines = Math.ceil(html.length / LINE_STR_COUNT);
|
||||
return lines * 16;
|
||||
},
|
||||
});
|
||||
|
||||
// Mock getComputedStyle
|
||||
const originGetComputedStyle = window.getComputedStyle;
|
||||
window.getComputedStyle = ele => {
|
||||
const style = originGetComputedStyle(ele);
|
||||
style.lineHeight = '16px';
|
||||
return style;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
errorSpy.mockReset();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
errorSpy.mockRestore();
|
||||
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
|
||||
get: originOffsetHeight,
|
||||
});
|
||||
window.getComputedStyle = originGetComputedStyle;
|
||||
});
|
||||
|
||||
describe('Base', () => {
|
||||
describe('trigger ellipsis update', () => {
|
||||
const fullStr =
|
||||
'Bamboo is Little Light Bamboo is Little Light Bamboo is Little Light Bamboo is Little Light Bamboo is Little Light';
|
||||
|
||||
it('should trigger update', async () => {
|
||||
const onEllipsis = jest.fn();
|
||||
const wrapper = mount(Base, {
|
||||
props: {
|
||||
ellipsis: { onEllipsis },
|
||||
component: 'p',
|
||||
editable: true,
|
||||
content: fullStr,
|
||||
},
|
||||
});
|
||||
|
||||
await sleep(20);
|
||||
|
||||
expect(wrapper.text()).toEqual('Bamboo is Little ...');
|
||||
expect(onEllipsis).toHaveBeenCalledWith(true);
|
||||
onEllipsis.mockReset();
|
||||
wrapper.setProps({ ellipsis: { rows: 2, onEllipsis } });
|
||||
await sleep(20);
|
||||
expect(wrapper.text()).toEqual('Bamboo is Little Light Bamboo is Litt...');
|
||||
expect(onEllipsis).not.toHaveBeenCalled();
|
||||
|
||||
wrapper.setProps({ ellipsis: { rows: 99, onEllipsis } });
|
||||
await sleep(20);
|
||||
expect(wrapper.find('p').text()).toEqual(fullStr);
|
||||
expect(onEllipsis).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('should middle ellipsis', async () => {
|
||||
const suffix = '--suffix';
|
||||
const wrapper = mount(Base, {
|
||||
props: {
|
||||
ellipsis: {
|
||||
rows: 1,
|
||||
suffix,
|
||||
},
|
||||
component: 'p',
|
||||
content: fullStr,
|
||||
},
|
||||
});
|
||||
|
||||
await sleep(20);
|
||||
expect(wrapper.find('p').text()).toEqual('Bamboo is...--suffix');
|
||||
});
|
||||
|
||||
it('should front or middle ellipsis', async () => {
|
||||
const suffix = '--The information is very important';
|
||||
const wrapper = mount(Base, {
|
||||
props: {
|
||||
ellipsis: {
|
||||
rows: 1,
|
||||
suffix,
|
||||
},
|
||||
component: 'p',
|
||||
content: fullStr,
|
||||
},
|
||||
});
|
||||
|
||||
await sleep(20);
|
||||
expect(wrapper.find('p').text()).toEqual('...--The information is very important');
|
||||
|
||||
wrapper.setProps({ ellipsis: { rows: 2, suffix } });
|
||||
await sleep(20);
|
||||
expect(wrapper.find('p').text()).toEqual('Ba...--The information is very important');
|
||||
|
||||
wrapper.setProps({ ellipsis: { rows: 99, suffix } });
|
||||
await sleep(20);
|
||||
expect(wrapper.find('p').text()).toEqual(fullStr + suffix);
|
||||
});
|
||||
|
||||
// it('connect children', async () => {
|
||||
// const bamboo = 'Bamboo';
|
||||
// const is = ' is ';
|
||||
|
||||
// const wrapper = mount(Base, {
|
||||
// props: {
|
||||
// ellipsis: true,
|
||||
// component: 'p',
|
||||
// editable: true,
|
||||
// },
|
||||
// slots: {
|
||||
// default: [
|
||||
// createTextVNode(bamboo),
|
||||
// createTextVNode(is),
|
||||
// createVNode('code', null, 'Little'),
|
||||
// createVNode('code', null, 'Light'),
|
||||
// ],
|
||||
// },
|
||||
// });
|
||||
|
||||
// await sleep(20);
|
||||
// expect(wrapper.find('span').text()).toEqual('Bamboo is Little...');
|
||||
// });
|
||||
|
||||
it('should expandable work', async () => {
|
||||
const onExpand = jest.fn();
|
||||
const wrapper = mount(Base, {
|
||||
props: {
|
||||
ellipsis: {
|
||||
expandable: true,
|
||||
onExpand,
|
||||
},
|
||||
component: 'p',
|
||||
copyable: true,
|
||||
editable: true,
|
||||
content: fullStr,
|
||||
},
|
||||
});
|
||||
|
||||
await sleep(20);
|
||||
wrapper.find('.ant-typography-expand').trigger('click');
|
||||
expect(onExpand).toHaveBeenCalled();
|
||||
await sleep(20);
|
||||
|
||||
expect(wrapper.find('p').text()).toEqual(fullStr);
|
||||
});
|
||||
|
||||
it('should have custom expand style', async () => {
|
||||
const symbol = 'more';
|
||||
const wrapper = mount(Base, {
|
||||
props: {
|
||||
ellipsis: {
|
||||
expandable: true,
|
||||
symbol,
|
||||
},
|
||||
component: 'p',
|
||||
content: fullStr,
|
||||
},
|
||||
});
|
||||
|
||||
await sleep(20);
|
||||
expect(wrapper.find('.ant-typography-expand').text()).toEqual('more');
|
||||
});
|
||||
|
||||
it('can use css ellipsis', async () => {
|
||||
const wrapper = mount(Base, {
|
||||
props: {
|
||||
ellipsis: true,
|
||||
component: 'p',
|
||||
},
|
||||
});
|
||||
|
||||
await sleep(20);
|
||||
expect(wrapper.findAll('.ant-typography-ellipsis-single-line').length).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('copyable', () => {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function copyTest(name, text, target, icon) {
|
||||
it(name, async () => {
|
||||
jest.useFakeTimers();
|
||||
const onCopy = jest.fn();
|
||||
const wrapper = mount(Base, {
|
||||
props: {
|
||||
component: 'p',
|
||||
copyable: { text, onCopy },
|
||||
},
|
||||
slots: {
|
||||
default: [createTextVNode('test copy')],
|
||||
copyableIcon: icon ? () => icon : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
if (icon) {
|
||||
expect(wrapper.findAll('.anticon-smile').length).toBeTruthy();
|
||||
} else {
|
||||
expect(wrapper.findAll('.anticon-copy').length).toBeTruthy();
|
||||
}
|
||||
|
||||
wrapper.find('.ant-typography-copy').trigger('click');
|
||||
|
||||
await asyncExpect(() => {
|
||||
expect(copy.lastStr).toEqual(target);
|
||||
});
|
||||
|
||||
await asyncExpect(() => {
|
||||
expect(onCopy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(wrapper.findAll('.anticon-check').length).toBeTruthy();
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
// Will set back when 3 seconds pass
|
||||
await nextTick();
|
||||
expect(wrapper.findAll('.anticon-check').length).toBeFalsy();
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
}
|
||||
|
||||
//copyTest('basic copy', undefined, 'test copy');
|
||||
//copyTest('customize copy', 'bamboo', 'bamboo');
|
||||
});
|
||||
|
||||
describe('editable', async () => {
|
||||
function testStep(name, submitFunc, expectFunc) {
|
||||
it(name, async () => {
|
||||
const onStart = jest.fn();
|
||||
const onChange = jest.fn();
|
||||
|
||||
const className = 'test';
|
||||
|
||||
const Component = {
|
||||
setup() {
|
||||
return () => (
|
||||
<Paragraph class={className} style={{ color: 'red' }}>
|
||||
Bamboo
|
||||
</Paragraph>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = mount(Component, {
|
||||
props: {
|
||||
editable: { onChange, onStart },
|
||||
},
|
||||
});
|
||||
|
||||
// Should have class
|
||||
const component = wrapper.find('div');
|
||||
expect(component.element.style.color).toEqual('red');
|
||||
expect(component.classes()).toContain(className);
|
||||
|
||||
wrapper.find('.ant-typography-edit').trigger('click');
|
||||
await sleep(20);
|
||||
expect(onStart).toHaveBeenCalled();
|
||||
|
||||
await sleep(20);
|
||||
wrapper.find('textarea').element.value = 'Bamboo';
|
||||
wrapper.find('textarea').trigger('change');
|
||||
|
||||
if (submitFunc) {
|
||||
submitFunc(wrapper);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (expectFunc) {
|
||||
expectFunc(onChange);
|
||||
} else {
|
||||
expect(onChange).toHaveBeenCalledWith('Bamboo');
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await testStep('by key up', async wrapper => {
|
||||
// Not trigger when inComposition
|
||||
wrapper.find('textarea').trigger('compositionstart');
|
||||
wrapper.find('textarea').trigger('keydown', { keyCode: KeyCode.ENTER });
|
||||
wrapper.find('textarea').trigger('compositionend');
|
||||
wrapper.find('textarea').trigger('keyup', { keyCode: KeyCode.ENTER });
|
||||
|
||||
// // Now trigger
|
||||
wrapper.find('textarea').trigger('keydown', { keyCode: KeyCode.ENTER });
|
||||
await sleep();
|
||||
wrapper.find('textarea').trigger('keyup', { keyCode: KeyCode.ENTER });
|
||||
});
|
||||
await testStep(
|
||||
'by esc key',
|
||||
async wrapper => {
|
||||
wrapper.find('textarea').trigger('keydown', { keyCode: KeyCode.ESC });
|
||||
await sleep();
|
||||
wrapper.find('textarea').trigger('keyup', { keyCode: KeyCode.ESC });
|
||||
},
|
||||
onChange => {
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
);
|
||||
|
||||
await testStep('by blur', wrapper => {
|
||||
wrapper.find('textarea').trigger('blur');
|
||||
});
|
||||
});
|
||||
|
||||
it('should focus at the end of textarea', async () => {
|
||||
const wrapper = mount(Paragraph, {
|
||||
props: {
|
||||
editable: true,
|
||||
content: 'content',
|
||||
},
|
||||
});
|
||||
await sleep();
|
||||
wrapper.find('.ant-typography-edit').trigger('click');
|
||||
await sleep();
|
||||
const textareaNode = wrapper.find('textarea').element;
|
||||
expect(textareaNode.selectionStart).toBe(7);
|
||||
expect(textareaNode.selectionEnd).toBe(7);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
import Typography from './Typography';
|
||||
|
||||
export default Typography;
|
|
@ -0,0 +1,283 @@
|
|||
@import '../../style/themes/index';
|
||||
@import '../../style/mixins/index';
|
||||
|
||||
@typography-prefix-cls: ~'@{ant-prefix}-typography';
|
||||
|
||||
// =============== Basic ===============
|
||||
.@{typography-prefix-cls} {
|
||||
color: @text-color;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
&&-secondary {
|
||||
color: @text-color-secondary;
|
||||
}
|
||||
|
||||
&&-success {
|
||||
color: @success-color;
|
||||
}
|
||||
|
||||
&&-warning {
|
||||
color: @warning-color;
|
||||
}
|
||||
|
||||
&&-danger {
|
||||
color: @error-color;
|
||||
a&:active,
|
||||
a&:focus,
|
||||
a&:hover {
|
||||
color: ~`colorPalette('@{error-color}', 5) `;
|
||||
}
|
||||
}
|
||||
|
||||
&&-disabled {
|
||||
color: @disabled-color;
|
||||
cursor: not-allowed;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
// Tag
|
||||
div&,
|
||||
p {
|
||||
.typography-paragraph();
|
||||
}
|
||||
|
||||
h1&,
|
||||
h1 {
|
||||
.typography-title-1();
|
||||
}
|
||||
h2&,
|
||||
h2 {
|
||||
.typography-title-2();
|
||||
}
|
||||
h3&,
|
||||
h3 {
|
||||
.typography-title-3();
|
||||
}
|
||||
h4&,
|
||||
h4 {
|
||||
.typography-title-4();
|
||||
}
|
||||
h5&,
|
||||
h5 {
|
||||
.typography-title-5();
|
||||
}
|
||||
|
||||
h1&,
|
||||
h2&,
|
||||
h3&,
|
||||
h4&,
|
||||
h5& {
|
||||
.@{typography-prefix-cls} + & {
|
||||
margin-top: @typography-title-margin-top;
|
||||
}
|
||||
}
|
||||
|
||||
div,
|
||||
ul,
|
||||
li,
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
+ h1,
|
||||
+ h2,
|
||||
+ h3,
|
||||
+ h4,
|
||||
+ h5 {
|
||||
margin-top: @typography-title-margin-top;
|
||||
}
|
||||
}
|
||||
|
||||
a&-ellipsis,
|
||||
span&-ellipsis {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
a&,
|
||||
a {
|
||||
.operation-unit();
|
||||
text-decoration: @link-decoration;
|
||||
|
||||
&:active,
|
||||
&:hover {
|
||||
text-decoration: @link-hover-decoration;
|
||||
}
|
||||
|
||||
&[disabled],
|
||||
&.@{typography-prefix-cls}-disabled {
|
||||
color: @disabled-color;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:active,
|
||||
&:hover {
|
||||
color: @disabled-color;
|
||||
}
|
||||
|
||||
&:active {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
margin: 0 0.2em;
|
||||
padding: 0.2em 0.4em 0.1em;
|
||||
font-size: 85%;
|
||||
background: rgba(150, 150, 150, 0.1);
|
||||
border: 1px solid rgba(100, 100, 100, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
kbd {
|
||||
margin: 0 0.2em;
|
||||
padding: 0.15em 0.4em 0.1em;
|
||||
font-size: 90%;
|
||||
background: rgba(150, 150, 150, 0.06);
|
||||
border: 1px solid rgba(100, 100, 100, 0.2);
|
||||
border-bottom-width: 2px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0;
|
||||
background-color: @gold-3;
|
||||
}
|
||||
|
||||
u,
|
||||
ins {
|
||||
text-decoration: underline;
|
||||
text-decoration-skip-ink: auto;
|
||||
}
|
||||
|
||||
s,
|
||||
del {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// Operation
|
||||
&-expand,
|
||||
&-edit,
|
||||
&-copy {
|
||||
.operation-unit();
|
||||
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
&-copy-success {
|
||||
&,
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: @success-color;
|
||||
}
|
||||
}
|
||||
|
||||
// Text input area
|
||||
&-edit-content {
|
||||
position: relative;
|
||||
|
||||
div& {
|
||||
left: -@input-padding-horizontal - 1px;
|
||||
margin-top: -@input-padding-vertical-base - 1px;
|
||||
// stylelint-disable-next-line function-calc-no-invalid
|
||||
margin-bottom: calc(1em - @input-padding-vertical-base - 1px);
|
||||
}
|
||||
|
||||
&-confirm {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 8px;
|
||||
color: @text-color-secondary;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Fix Editable Textarea flash in Firefox
|
||||
textarea {
|
||||
-moz-transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
// list
|
||||
ul,
|
||||
ol {
|
||||
margin: 0 0 1em 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
margin: 0 0 0 20px;
|
||||
padding: 0 0 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: circle;
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
// pre & block
|
||||
pre,
|
||||
blockquote {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 0.4em 0.6em;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
background: rgba(150, 150, 150, 0.1);
|
||||
border: 1px solid rgba(100, 100, 100, 0.2);
|
||||
border-radius: 3px;
|
||||
|
||||
// Compatible for marked
|
||||
code {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding: 0 0 0 0.6em;
|
||||
border-left: 4px solid rgba(100, 100, 100, 0.2);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
// ============ Ellipsis ============
|
||||
&-ellipsis-single-line {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
// https://blog.csdn.net/iefreer/article/details/50421025
|
||||
a&,
|
||||
span& {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
|
||||
&-ellipsis-multiple-line {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 3;
|
||||
/*! autoprefixer: ignore next */
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
|
||||
@import './rtl';
|
|
@ -0,0 +1,6 @@
|
|||
import '../../style/index.less';
|
||||
import './index.less';
|
||||
|
||||
// style dependencies
|
||||
import '../../tooltip/style';
|
||||
import '../../input/style';
|
|
@ -0,0 +1,54 @@
|
|||
@import '../../style/themes/index';
|
||||
@import '../../style/mixins/index';
|
||||
|
||||
@typography-prefix-cls: ~'@{ant-prefix}-typography';
|
||||
|
||||
.@{typography-prefix-cls} {
|
||||
&-rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
// Operation
|
||||
&-expand,
|
||||
&-edit,
|
||||
&-copy {
|
||||
.@{typography-prefix-cls}-rtl & {
|
||||
margin-right: 4px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-expand {
|
||||
.@{typography-prefix-cls}-rtl & {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
// Text input area
|
||||
&-edit-content {
|
||||
div& {
|
||||
&.@{typography-prefix-cls}-rtl {
|
||||
right: -@input-padding-horizontal - 1px;
|
||||
left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&-confirm {
|
||||
.@{typography-prefix-cls}-rtl & {
|
||||
right: auto;
|
||||
left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// list
|
||||
ul,
|
||||
ol {
|
||||
li {
|
||||
.@{typography-prefix-cls}-rtl& {
|
||||
margin: 0 20px 0 0;
|
||||
padding: 0 4px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,223 @@
|
|||
import { createApp, CSSProperties, VNodeTypes } from 'vue';
|
||||
|
||||
interface MeasureResult {
|
||||
finished: boolean;
|
||||
vNode: VNodeTypes;
|
||||
}
|
||||
interface Option {
|
||||
rows: number;
|
||||
suffix?: string;
|
||||
}
|
||||
|
||||
// We only handle element & text node.
|
||||
const TEXT_NODE = 3;
|
||||
const COMMENT_NODE = 8;
|
||||
|
||||
let ellipsisContainer: HTMLParagraphElement;
|
||||
|
||||
const wrapperStyle: CSSProperties = {
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
display: 'inline',
|
||||
lineHeight: 'inherit',
|
||||
};
|
||||
|
||||
function pxToNumber(value: string | null) {
|
||||
if (!value) return 0;
|
||||
|
||||
const match = value.match(/^\d*(\.\d*)?/);
|
||||
|
||||
return match ? Number(match[0]) : 0;
|
||||
}
|
||||
|
||||
function styleToString(style: CSSStyleDeclaration) {
|
||||
// There are some different behavior between Firefox & Chrome.
|
||||
// We have to handle this ourself.
|
||||
const styleNames = Array.prototype.slice.apply(style);
|
||||
return styleNames.map(name => `${name}: ${style.getPropertyValue(name)};`).join('');
|
||||
}
|
||||
|
||||
export default (
|
||||
originEle: HTMLElement,
|
||||
option: Option,
|
||||
content: string,
|
||||
fixedContent: VNodeTypes[],
|
||||
ellipsisStr: string,
|
||||
): {
|
||||
content: VNodeTypes;
|
||||
text: string;
|
||||
ellipsis: boolean;
|
||||
} => {
|
||||
if (!ellipsisContainer) {
|
||||
ellipsisContainer = document.createElement('div');
|
||||
ellipsisContainer.setAttribute('aria-hidden', 'true');
|
||||
document.body.appendChild(ellipsisContainer);
|
||||
}
|
||||
|
||||
const { rows, suffix = '' } = option;
|
||||
// Get origin style
|
||||
const originStyle = window.getComputedStyle(originEle);
|
||||
const originCSS = styleToString(originStyle);
|
||||
const lineHeight = pxToNumber(originStyle.lineHeight);
|
||||
const maxHeight = Math.round(
|
||||
lineHeight * (rows + 1) +
|
||||
pxToNumber(originStyle.paddingTop) +
|
||||
pxToNumber(originStyle.paddingBottom),
|
||||
);
|
||||
|
||||
// Set shadow
|
||||
ellipsisContainer.setAttribute('style', originCSS);
|
||||
ellipsisContainer.style.position = 'fixed';
|
||||
ellipsisContainer.style.left = '0';
|
||||
ellipsisContainer.style.height = 'auto';
|
||||
ellipsisContainer.style.minHeight = 'auto';
|
||||
ellipsisContainer.style.maxHeight = 'auto';
|
||||
ellipsisContainer.style.top = '-999999px';
|
||||
ellipsisContainer.style.zIndex = '-1000';
|
||||
|
||||
// clean up css overflow
|
||||
ellipsisContainer.style.textOverflow = 'clip';
|
||||
ellipsisContainer.style.whiteSpace = 'normal';
|
||||
ellipsisContainer.style.webkitLineClamp = 'none';
|
||||
|
||||
// Render in the fake container
|
||||
const vm = createApp({
|
||||
render() {
|
||||
return (
|
||||
<div style={wrapperStyle}>
|
||||
<span style={wrapperStyle}>
|
||||
{content}
|
||||
{suffix}
|
||||
</span>
|
||||
<span style={wrapperStyle}>{fixedContent}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
vm.mount(ellipsisContainer);
|
||||
|
||||
// Check if ellipsis in measure div is height enough for content
|
||||
function inRange() {
|
||||
return ellipsisContainer.offsetHeight < maxHeight;
|
||||
}
|
||||
|
||||
// Skip ellipsis if already match
|
||||
if (inRange()) {
|
||||
vm.unmount();
|
||||
return { content, text: ellipsisContainer.innerHTML, ellipsis: false };
|
||||
}
|
||||
const childNodes = Array.prototype.slice
|
||||
.apply(ellipsisContainer.childNodes[0].childNodes[0].cloneNode(true).childNodes)
|
||||
.filter(({ nodeType, data }) => nodeType !== COMMENT_NODE && data !== '');
|
||||
const fixedNodes = Array.prototype.slice.apply(
|
||||
ellipsisContainer.childNodes[0].childNodes[1].cloneNode(true).childNodes,
|
||||
);
|
||||
|
||||
vm.unmount();
|
||||
|
||||
// ========================= Find match ellipsis content =========================
|
||||
const ellipsisChildren = [];
|
||||
ellipsisContainer.innerHTML = '';
|
||||
|
||||
// Create origin content holder
|
||||
const ellipsisContentHolder = document.createElement('span');
|
||||
ellipsisContainer.appendChild(ellipsisContentHolder);
|
||||
const ellipsisTextNode = document.createTextNode(ellipsisStr + suffix);
|
||||
ellipsisContentHolder.appendChild(ellipsisTextNode);
|
||||
|
||||
fixedNodes.forEach(childNode => {
|
||||
ellipsisContainer.appendChild(childNode);
|
||||
});
|
||||
|
||||
// Append before fixed nodes
|
||||
function appendChildNode(node: ChildNode) {
|
||||
ellipsisContentHolder.insertBefore(node, ellipsisTextNode);
|
||||
}
|
||||
|
||||
// Get maximum text
|
||||
function measureText(
|
||||
textNode: Text,
|
||||
fullText: string,
|
||||
startLoc = 0,
|
||||
endLoc = fullText.length,
|
||||
lastSuccessLoc = 0,
|
||||
): MeasureResult {
|
||||
const midLoc = Math.floor((startLoc + endLoc) / 2);
|
||||
const currentText = fullText.slice(0, midLoc);
|
||||
textNode.textContent = currentText;
|
||||
|
||||
if (startLoc >= endLoc - 1) {
|
||||
// Loop when step is small
|
||||
for (let step = endLoc; step >= startLoc; step -= 1) {
|
||||
const currentStepText = fullText.slice(0, step);
|
||||
textNode.textContent = currentStepText;
|
||||
|
||||
if (inRange() || !currentStepText) {
|
||||
return step === fullText.length
|
||||
? {
|
||||
finished: false,
|
||||
vNode: fullText,
|
||||
}
|
||||
: {
|
||||
finished: true,
|
||||
vNode: currentStepText,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inRange()) {
|
||||
return measureText(textNode, fullText, midLoc, endLoc, midLoc);
|
||||
}
|
||||
return measureText(textNode, fullText, startLoc, midLoc, lastSuccessLoc);
|
||||
}
|
||||
|
||||
function measureNode(childNode: ChildNode): MeasureResult {
|
||||
const type = childNode.nodeType;
|
||||
// console.log('type', type);
|
||||
// if (type === ELEMENT_NODE) {
|
||||
// // We don't split element, it will keep if whole element can be displayed.
|
||||
// appendChildNode(childNode);
|
||||
// if (inRange()) {
|
||||
// return {
|
||||
// finished: false,
|
||||
// vNode: contentList[index],
|
||||
// };
|
||||
// }
|
||||
|
||||
// // Clean up if can not pull in
|
||||
// ellipsisContentHolder.removeChild(childNode);
|
||||
// return {
|
||||
// finished: true,
|
||||
// vNode: null,
|
||||
// };
|
||||
// }
|
||||
if (type === TEXT_NODE) {
|
||||
const fullText = childNode.textContent || '';
|
||||
const textNode = document.createTextNode(fullText);
|
||||
appendChildNode(textNode);
|
||||
return measureText(textNode, fullText);
|
||||
}
|
||||
|
||||
// Not handle other type of content
|
||||
return {
|
||||
finished: false,
|
||||
vNode: null,
|
||||
};
|
||||
}
|
||||
|
||||
childNodes.some(childNode => {
|
||||
const { finished, vNode } = measureNode(childNode);
|
||||
if (vNode) {
|
||||
ellipsisChildren.push(vNode);
|
||||
}
|
||||
return finished;
|
||||
});
|
||||
|
||||
return {
|
||||
content: ellipsisChildren,
|
||||
text: ellipsisContainer.innerHTML,
|
||||
ellipsis: true,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
import { VNodeTypes } from '@vue/runtime-core';
|
||||
import { isFragment } from '../../_util/props-util';
|
||||
|
||||
export interface Option {
|
||||
keepEmpty?: boolean;
|
||||
}
|
||||
|
||||
export default function toArray(children: any[], option: Option = {}): any[] {
|
||||
let ret: VNodeTypes[] = [];
|
||||
|
||||
children.forEach((child: any) => {
|
||||
if ((child === undefined || child === null) && !option.keepEmpty) {
|
||||
return;
|
||||
}
|
||||
debugger;
|
||||
if (Array.isArray(child)) {
|
||||
ret = ret.concat(toArray(child));
|
||||
} else if (isFragment(child) && child.props) {
|
||||
ret = ret.concat(toArray(child.props.children, option));
|
||||
} else {
|
||||
ret.push(child);
|
||||
}
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
</template>
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import demo from '../v2-doc/src/docs/affix/demo/basic.vue';
|
||||
import demo from '../v2-doc/src/docs/typography/demo/index.vue';
|
||||
// import Affix from '../components/affix';
|
||||
export default defineComponent({
|
||||
components: {
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
function copy(str) {
|
||||
copy.lastStr = str;
|
||||
}
|
||||
|
||||
export default copy;
|
|
@ -67,6 +67,7 @@ Array [
|
|||
"Descriptions",
|
||||
"PageHeader",
|
||||
"Space",
|
||||
"Typography",
|
||||
"install",
|
||||
"default",
|
||||
]
|
||||
|
|
2
v2-doc
2
v2-doc
|
@ -1 +1 @@
|
|||
Subproject commit 0468ad3010f71ad6b267c66c4f5e28c89c19d83e
|
||||
Subproject commit 77d4fda0cd6e2d56704a2d6d789631d269a56a46
|
|
@ -48,6 +48,9 @@ module.exports = {
|
|||
entry: {
|
||||
app: './examples/index.js',
|
||||
},
|
||||
stats: {
|
||||
warningsFilter: /export .* was not found in/,
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue