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
tangjinzhou 2021-03-16 12:54:22 +08:00 committed by GitHub
parent 21bf0a3a40
commit b0025d9e79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 2145 additions and 3 deletions

View File

@ -36,7 +36,8 @@
"@typescript-eslint/no-unused-vars": [
"error",
{ "vars": "all", "args": "after-used", "ignoreRestSiblings": true }
]
],
"@typescript-eslint/ban-ts-comment": 0
}
}
],

View File

@ -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;

View File

@ -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;

View File

@ -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 };
};

View File

@ -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 {

View File

@ -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({

View File

@ -155,5 +155,6 @@ export default function calculateNodeHeight(
minHeight: `${minHeight}px`,
maxHeight: `${maxHeight}px`,
overflowY,
resize: 'none',
};
}

View File

@ -61,4 +61,5 @@ import './page-header/style';
import './form/style';
import './space/style';
import './image/style';
import './typography/style';
// import './color-picker/style';

View File

@ -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
);
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
};

View File

@ -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>
`;

View File

@ -0,0 +1,3 @@
import demoTest from '../../../tests/shared/demoTest';
demoTest('typography');

View File

@ -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);
});
});
});

View File

@ -0,0 +1,3 @@
import Typography from './Typography';
export default Typography;

View File

@ -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';

View File

@ -0,0 +1,6 @@
import '../../style/index.less';
import './index.less';
// style dependencies
import '../../tooltip/style';
import '../../input/style';

View File

@ -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;
}
}
}
}

View File

@ -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,
};
};

View File

@ -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;
}

View File

@ -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: {

View File

@ -0,0 +1,5 @@
function copy(str) {
copy.lastStr = str;
}
export default copy;

View File

@ -67,6 +67,7 @@ Array [
"Descriptions",
"PageHeader",
"Space",
"Typography",
"install",
"default",
]

2
v2-doc

@ -1 +1 @@
Subproject commit 0468ad3010f71ad6b267c66c4f5e28c89c19d83e
Subproject commit 77d4fda0cd6e2d56704a2d6d789631d269a56a46

View File

@ -48,6 +48,9 @@ module.exports = {
entry: {
app: './examples/index.js',
},
stats: {
warningsFilter: /export .* was not found in/,
},
module: {
rules: [
{