From 5a5aa8b3362c800601791af08e4767068f6c66be Mon Sep 17 00:00:00 2001 From: Hsuan Lee Date: Wed, 26 Jun 2019 19:13:13 +0800 Subject: [PATCH] feat(module:typography): add typography component (#3119) * feat(module:typography): add typography component * test(module:typography): add test * style: fix lint * docs: add static path * test(module:typography): add test * docs: fix API * test(module:typography): fix ci test * test(module:typography): fix ci test * fix(module:typography): fix ellipsis content * Update components/components.less Co-Authored-By: vthinkxie --- components/components.less | 1 + components/core/public-api.ts | 1 + .../services/nz-copy-to-clipboard.service.ts | 52 +++ components/core/services/public-api.ts | 1 + components/core/trans-button/index.ts | 9 + .../trans-button/nz-trans-button.directive.ts | 20 + components/core/trans-button/public-api.ts | 9 + components/core/util/public-api.ts | 2 + components/core/util/style-checke.ts | 17 + components/core/util/text-measure.ts | 244 +++++++++++ components/i18n/languages/en_US.ts | 9 + components/i18n/languages/es_ES.ts | 9 + components/i18n/languages/it_IT.ts | 6 + components/i18n/languages/ru_RU.ts | 9 + components/i18n/languages/tr_TR.ts | 6 + components/i18n/languages/zh_CN.ts | 9 + components/i18n/languages/zh_TW.ts | 3 + components/icon/nz-icon.service.ts | 4 + components/input/nz-autosize.directive.ts | 2 +- components/ng-zorro-antd.module.ts | 5 +- components/typography/demo/basic.md | 14 + components/typography/demo/basic.ts | 83 ++++ components/typography/demo/ellipsis.md | 14 + components/typography/demo/ellipsis.ts | 41 ++ components/typography/demo/interactive.md | 14 + components/typography/demo/interactive.ts | 14 + components/typography/demo/text.md | 14 + components/typography/demo/text.ts | 30 ++ components/typography/demo/title.md | 14 + components/typography/demo/title.ts | 15 + components/typography/doc/index.en-US.md | 31 ++ components/typography/doc/index.zh-CN.md | 31 ++ components/typography/index.ts | 9 + .../typography/nz-text-copy.component.html | 11 + .../typography/nz-text-copy.component.ts | 84 ++++ .../typography/nz-text-edit.component.html | 22 + .../typography/nz-text-edit.component.ts | 103 +++++ .../typography/nz-typography.component.html | 24 ++ .../typography/nz-typography.component.ts | 249 +++++++++++ components/typography/nz-typography.module.ts | 27 ++ components/typography/nz-typography.spec.ts | 398 ++++++++++++++++++ components/typography/package.json | 7 + components/typography/public-api.ts | 12 + components/typography/style/entry.less | 5 + components/typography/style/index.less | 225 ++++++++++ scripts/prerender/static.paths.ts | 2 + 46 files changed, 1909 insertions(+), 2 deletions(-) create mode 100644 components/core/services/nz-copy-to-clipboard.service.ts create mode 100644 components/core/trans-button/index.ts create mode 100755 components/core/trans-button/nz-trans-button.directive.ts create mode 100644 components/core/trans-button/public-api.ts create mode 100644 components/core/util/style-checke.ts create mode 100644 components/core/util/text-measure.ts create mode 100644 components/typography/demo/basic.md create mode 100644 components/typography/demo/basic.ts create mode 100644 components/typography/demo/ellipsis.md create mode 100644 components/typography/demo/ellipsis.ts create mode 100644 components/typography/demo/interactive.md create mode 100644 components/typography/demo/interactive.ts create mode 100644 components/typography/demo/text.md create mode 100644 components/typography/demo/text.ts create mode 100644 components/typography/demo/title.md create mode 100644 components/typography/demo/title.ts create mode 100644 components/typography/doc/index.en-US.md create mode 100644 components/typography/doc/index.zh-CN.md create mode 100644 components/typography/index.ts create mode 100644 components/typography/nz-text-copy.component.html create mode 100644 components/typography/nz-text-copy.component.ts create mode 100644 components/typography/nz-text-edit.component.html create mode 100644 components/typography/nz-text-edit.component.ts create mode 100644 components/typography/nz-typography.component.html create mode 100644 components/typography/nz-typography.component.ts create mode 100644 components/typography/nz-typography.module.ts create mode 100644 components/typography/nz-typography.spec.ts create mode 100644 components/typography/package.json create mode 100644 components/typography/public-api.ts create mode 100644 components/typography/style/entry.less create mode 100644 components/typography/style/index.less diff --git a/components/components.less b/components/components.less index ce82446ad8..4a739371a3 100644 --- a/components/components.less +++ b/components/components.less @@ -50,6 +50,7 @@ @import "./timeline/style/entry.less"; @import "./tooltip/style/entry.less"; @import "./transfer/style/entry.less"; +@import "./typography/style/entry.less"; @import "./upload/style/entry.less"; @import "./auto-complete/style/entry.less"; @import "./cascader/style/entry.less"; diff --git a/components/core/public-api.ts b/components/core/public-api.ts index bf33ab9b4e..e76d3260ba 100644 --- a/components/core/public-api.ts +++ b/components/core/public-api.ts @@ -22,3 +22,4 @@ export * from './wave/public-api'; export * from './dropdown/public-api'; export * from './logger/public-api'; export * from './responsive/public-api'; +export * from './trans-button/public-api'; diff --git a/components/core/services/nz-copy-to-clipboard.service.ts b/components/core/services/nz-copy-to-clipboard.service.ts new file mode 100644 index 0000000000..51133745c4 --- /dev/null +++ b/components/core/services/nz-copy-to-clipboard.service.ts @@ -0,0 +1,52 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { DOCUMENT } from '@angular/common'; +import { Inject, Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class NzCopyToClipboardService { + // tslint:disable-next-line:no-any + constructor(@Inject(DOCUMENT) private document: any) {} + + copy(text: string): Promise { + return new Promise( + (resolve, reject): void => { + let copyTextArea = null; + try { + // tslint:disable-next-line no-any + copyTextArea = this.document.createElement('textarea') as any; + copyTextArea.style!.all = 'unset'; + copyTextArea.style.position = 'fixed'; + copyTextArea.style.top = '0'; + copyTextArea.style.clip = 'rect(0, 0, 0, 0)'; + copyTextArea.style.whiteSpace = 'pre'; + copyTextArea.style.webkitUserSelect = 'text'; + copyTextArea.style!.MozUserSelect = 'text'; + copyTextArea.style.msUserSelect = 'text'; + copyTextArea.style.userSelect = 'text'; + this.document.body.appendChild(copyTextArea); + copyTextArea.value = text; + copyTextArea.select(); + + const successful = this.document.execCommand('copy'); + if (!successful) { + reject(text); + } + resolve(text); + } finally { + if (copyTextArea) { + this.document.body.removeChild(copyTextArea); + } + } + } + ); + } +} diff --git a/components/core/services/public-api.ts b/components/core/services/public-api.ts index 185367cbd4..b7976ddc77 100644 --- a/components/core/services/public-api.ts +++ b/components/core/services/public-api.ts @@ -8,3 +8,4 @@ export * from './nz-measure-scrollbar.service'; export * from './update-host-class.service'; +export * from './nz-copy-to-clipboard.service'; diff --git a/components/core/trans-button/index.ts b/components/core/trans-button/index.ts new file mode 100644 index 0000000000..f17e95188c --- /dev/null +++ b/components/core/trans-button/index.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +export * from './public-api'; diff --git a/components/core/trans-button/nz-trans-button.directive.ts b/components/core/trans-button/nz-trans-button.directive.ts new file mode 100755 index 0000000000..4b0b243cd0 --- /dev/null +++ b/components/core/trans-button/nz-trans-button.directive.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { Directive } from '@angular/core'; + +@Directive({ + selector: 'button[nz-trans-button]', + host: { + '[style.border]': '"0"', + '[style.background]': '"transparent"', + '[style.padding]': '"0"', + '[style.line-height]': '"inherit"' + } +}) +export class NzTransButtonDirective {} diff --git a/components/core/trans-button/public-api.ts b/components/core/trans-button/public-api.ts new file mode 100644 index 0000000000..d71b8eaee5 --- /dev/null +++ b/components/core/trans-button/public-api.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +export { NzTransButtonDirective } from './nz-trans-button.directive'; diff --git a/components/core/util/public-api.ts b/components/core/util/public-api.ts index 987168ee90..010dc5e55e 100644 --- a/components/core/util/public-api.ts +++ b/components/core/util/public-api.ts @@ -18,3 +18,5 @@ export * from './scroll-into-view-if-needed'; export * from './textarea-caret-position'; export * from './throttleByAnimationFrame'; export * from './time'; +export * from './style-checke'; +export * from './text-measure'; diff --git a/components/core/util/style-checke.ts b/components/core/util/style-checke.ts new file mode 100644 index 0000000000..30ed9148f3 --- /dev/null +++ b/components/core/util/style-checke.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +export function isStyleSupport(styleName: string | string[]): boolean { + if (typeof window !== 'undefined' && window.document && window.document.documentElement) { + const styleNameList = Array.isArray(styleName) ? styleName : [styleName]; + const { documentElement } = window.document; + + return styleNameList.some(name => name in documentElement.style); + } + return false; +} diff --git a/components/core/util/text-measure.ts b/components/core/util/text-measure.ts new file mode 100644 index 0000000000..e2dfffeb2a --- /dev/null +++ b/components/core/util/text-measure.ts @@ -0,0 +1,244 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +export interface MeasureResult { + finished: boolean; + node: Node | null; +} + +// We only handle element & text node. +const ELEMENT_NODE = 1; +const TEXT_NODE = 3; +const COMMENT_NODE = 8; + +let ellipsisContainer: HTMLParagraphElement; + +const wrapperStyle = { + padding: '0', + margin: '0', + display: 'inline', + lineHeight: 'inherit' +}; + +function pxToNumber(value: string | null): number { + if (!value) { + return 0; + } + + const match = value.match(/^\d*(\.\d*)?/); + + return match ? Number(match[0]) : 0; +} + +function styleToString(style: CSSStyleDeclaration): string { + // There are some different behavior between Firefox & Chrome. + // We have to handle this ourself. + const styleNames: string[] = Array.prototype.slice.apply(style); + return styleNames.map(name => `${name}: ${style.getPropertyValue(name)};`).join(''); +} + +function mergeChildren(children: Node[]): Node[] { + const childList: Node[] = []; + + children.forEach((child: Node) => { + const prevChild = childList[childList.length - 1]; + if (prevChild && child.nodeType === TEXT_NODE && prevChild.nodeType === TEXT_NODE) { + (prevChild as Text).data += (child as Text).data; + } else { + childList.push(child); + } + }); + + return childList; +} + +export function measure( + originEle: HTMLElement, + rows: number, + contentNodes: Node[], + fixedContent: HTMLElement[], + ellipsisStr: string +): { contentNodes: Node[]; text: string; ellipsis: boolean } { + if (!ellipsisContainer) { + ellipsisContainer = document.createElement('div'); + ellipsisContainer.setAttribute('aria-hidden', 'true'); + document.body.appendChild(ellipsisContainer); + } + + // Get origin style + const originStyle = window.getComputedStyle(originEle); + const originCSS = styleToString(originStyle); + const lineHeight = pxToNumber(originStyle.lineHeight); + const maxHeight = + 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'; + // tslint:disable-next-line no-any + (ellipsisContainer.style as any).webkitLineClamp = 'none'; + + const contentList = mergeChildren(contentNodes); + const container = document.createElement('div'); + const contentContainer = document.createElement('span'); + const fixedContainer = document.createElement('span'); + + // Add styles in container + Object.assign(container.style, wrapperStyle); + Object.assign(contentContainer.style, wrapperStyle); + Object.assign(fixedContainer.style, wrapperStyle); + + contentList.forEach(n => { + contentContainer.appendChild(n); + }); + fixedContent.forEach(node => { + fixedContainer.appendChild(node.cloneNode(true)); + }); + container.appendChild(contentContainer); + container.appendChild(fixedContainer); + + // Render in the fake container + ellipsisContainer.appendChild(container); + + // Check if ellipsis in measure div is height enough for content + function inRange(): boolean { + return ellipsisContainer.offsetHeight < maxHeight; + } + + if (inRange()) { + const text = ellipsisContainer.innerHTML; + ellipsisContainer.removeChild(container); + return { contentNodes, text, ellipsis: false }; + } + + // We should clone the childNode since they're controlled by React and we can't reuse it without warning + const childNodes: ChildNode[] = Array.prototype.slice + .apply(ellipsisContainer.childNodes[0].childNodes[0].cloneNode(true).childNodes) + .filter(({ nodeType }: ChildNode) => nodeType !== COMMENT_NODE); + const fixedNodes: ChildNode[] = Array.prototype.slice.apply( + ellipsisContainer.childNodes[0].childNodes[1].cloneNode(true).childNodes + ); + ellipsisContainer.removeChild(container); + + // ========================= Find match ellipsis content ========================= + ellipsisContainer.innerHTML = ''; + + // Create origin content holder + const ellipsisContentHolder = document.createElement('span'); + ellipsisContainer.appendChild(ellipsisContentHolder); + const ellipsisTextNode = document.createTextNode(ellipsisStr); + ellipsisContentHolder.appendChild(ellipsisTextNode); + + fixedNodes.forEach(childNode => { + ellipsisContainer.appendChild(childNode); + }); + + // Append before fixed nodes + function appendChildNode(node: ChildNode): void { + ellipsisContentHolder.insertBefore(node, ellipsisTextNode); + } + + // Get maximum text + function measureText( + textNode: Text, + fullText: string, + startLoc: number = 0, + endLoc: number = fullText.length, + lastSuccessLoc: number = 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()) { + return step === fullText.length + ? { + finished: false, + node: document.createTextNode(fullText) + } + : { + finished: true, + node: document.createTextNode(currentStepText) + }; + } + } + } + if (inRange()) { + return measureText(textNode, fullText, midLoc, endLoc, midLoc); + } else { + return measureText(textNode, fullText, startLoc, midLoc, lastSuccessLoc); + } + } + + function measureNode(childNode: ChildNode, index: number): MeasureResult { + const type = childNode.nodeType; + + 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, + node: contentList[index] + }; + } + + // Clean up if can not pull in + ellipsisContentHolder.removeChild(childNode); + return { + finished: true, + node: null + }; + } else 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 + // PS: This code should not be attached after react 16 + return { + finished: false, + node: null + }; + } + + const ellipsisNodes: Node[] = []; + childNodes.some((childNode, index) => { + const { finished, node } = measureNode(childNode, index); + if (node) { + ellipsisNodes.push(node); + } + return finished; + }); + const result = { + contentNodes: ellipsisNodes, + text: ellipsisContainer.innerHTML, + ellipsis: true + }; + while (ellipsisContainer.firstChild) { + ellipsisContainer.removeChild(ellipsisContainer.firstChild); + } + return result; +} diff --git a/components/i18n/languages/en_US.ts b/components/i18n/languages/en_US.ts index 02cae05b95..2e487c393f 100644 --- a/components/i18n/languages/en_US.ts +++ b/components/i18n/languages/en_US.ts @@ -51,5 +51,14 @@ export default { }, Empty: { description: 'No Data' + }, + Text: { + edit: 'edit', + copy: 'copy', + copied: 'copy success', + expand: 'expand' + }, + PageHeader: { + back: 'back' } }; diff --git a/components/i18n/languages/es_ES.ts b/components/i18n/languages/es_ES.ts index 5b18a37398..17b8dc6bd4 100644 --- a/components/i18n/languages/es_ES.ts +++ b/components/i18n/languages/es_ES.ts @@ -46,5 +46,14 @@ export default { }, Empty: { description: 'No hay datos' + }, + Text: { + edit: 'editar', + copy: 'copiar', + copied: 'copiado', + expand: 'expandir' + }, + PageHeader: { + back: 'volver' } }; diff --git a/components/i18n/languages/it_IT.ts b/components/i18n/languages/it_IT.ts index 7a7a563d28..3ed116ec6b 100644 --- a/components/i18n/languages/it_IT.ts +++ b/components/i18n/languages/it_IT.ts @@ -47,5 +47,11 @@ export default { }, Empty: { description: 'Nessun dato' + }, + Text: { + edit: 'modifica', + copy: 'copia', + copied: 'copia effettuata', + expand: 'espandi' } }; diff --git a/components/i18n/languages/ru_RU.ts b/components/i18n/languages/ru_RU.ts index 801441a795..cf14713b14 100644 --- a/components/i18n/languages/ru_RU.ts +++ b/components/i18n/languages/ru_RU.ts @@ -46,5 +46,14 @@ export default { }, Empty: { description: 'Нет данных' + }, + Text: { + edit: 'редактировать', + copy: 'копировать', + copied: 'скопировано', + expand: 'раскрыть' + }, + PageHeader: { + back: 'назад' } }; diff --git a/components/i18n/languages/tr_TR.ts b/components/i18n/languages/tr_TR.ts index c9caa1e6f9..c761c0e065 100644 --- a/components/i18n/languages/tr_TR.ts +++ b/components/i18n/languages/tr_TR.ts @@ -46,5 +46,11 @@ export default { }, Empty: { description: 'Veri Yok' + }, + Text: { + edit: 'düzenle', + copy: 'kopyala', + copied: 'kopyalandı', + expand: 'genişlet' } }; diff --git a/components/i18n/languages/zh_CN.ts b/components/i18n/languages/zh_CN.ts index 409c6a44f8..f4d5ede807 100644 --- a/components/i18n/languages/zh_CN.ts +++ b/components/i18n/languages/zh_CN.ts @@ -51,5 +51,14 @@ export default { }, Empty: { description: '暂无数据' + }, + Text: { + edit: '编辑', + copy: '复制', + copied: '复制成功', + expand: '展开' + }, + PageHeader: { + back: '返回' } }; diff --git a/components/i18n/languages/zh_TW.ts b/components/i18n/languages/zh_TW.ts index 1199cf2da7..0b6b15b9fa 100644 --- a/components/i18n/languages/zh_TW.ts +++ b/components/i18n/languages/zh_TW.ts @@ -46,5 +46,8 @@ export default { }, Empty: { description: '無此資料' + }, + PageHeader: { + back: '返回' } }; diff --git a/components/icon/nz-icon.service.ts b/components/icon/nz-icon.service.ts index 35f314edcd..f89d04f74e 100644 --- a/components/icon/nz-icon.service.ts +++ b/components/icon/nz-icon.service.ts @@ -25,9 +25,11 @@ import { CloseCircleFill, CloseCircleOutline, CloseOutline, + CopyOutline, DoubleLeftOutline, DoubleRightOutline, DownOutline, + EditOutline, EllipsisOutline, ExclamationCircleFill, ExclamationCircleOutline, @@ -70,9 +72,11 @@ export const NZ_ICONS_USED_BY_ZORRO: IconDefinition[] = [ CloseCircleOutline, CloseCircleFill, CloseOutline, + CopyOutline, DoubleLeftOutline, DoubleRightOutline, DownOutline, + EditOutline, EllipsisOutline, ExclamationCircleFill, ExclamationCircleOutline, diff --git a/components/input/nz-autosize.directive.ts b/components/input/nz-autosize.directive.ts index 321fa6a9f2..5eda45a6bb 100644 --- a/components/input/nz-autosize.directive.ts +++ b/components/input/nz-autosize.directive.ts @@ -144,7 +144,7 @@ export class NzAutosizeDirective implements AfterViewInit, OnDestroy, DoCheck { textareaClone.style.overflow = 'hidden'; this.el.parentNode!.appendChild(textareaClone); - this.cachedLineHeight = textareaClone.clientHeight - this.inputGap - 1; + this.cachedLineHeight = textareaClone.clientHeight; this.el.parentNode!.removeChild(textareaClone); // Min and max heights have to be re-calculated if the cached line height changes diff --git a/components/ng-zorro-antd.module.ts b/components/ng-zorro-antd.module.ts index 1bb926c789..b5e1c7a220 100644 --- a/components/ng-zorro-antd.module.ts +++ b/components/ng-zorro-antd.module.ts @@ -59,6 +59,7 @@ import { NzToolTipModule } from 'ng-zorro-antd/tooltip'; import { NzTransferModule } from 'ng-zorro-antd/transfer'; import { NzTreeModule } from 'ng-zorro-antd/tree'; import { NzTreeSelectModule } from 'ng-zorro-antd/tree-select'; +import { NzTypographyModule } from 'ng-zorro-antd/typography'; import { NzUploadModule } from 'ng-zorro-antd/upload'; export * from 'ng-zorro-antd/affix'; @@ -122,6 +123,7 @@ export * from 'ng-zorro-antd/tooltip'; export * from 'ng-zorro-antd/transfer'; export * from 'ng-zorro-antd/tree-select'; export * from 'ng-zorro-antd/tree'; +export * from 'ng-zorro-antd/typography'; export * from 'ng-zorro-antd/upload'; export * from './version'; @@ -188,7 +190,8 @@ export * from './version'; NzSkeletonModule, NzStatisticModule, NzEmptyModule, - NzDescriptionsModule + NzDescriptionsModule, + NzTypographyModule ] }) export class NgZorroAntdModule { diff --git a/components/typography/demo/basic.md b/components/typography/demo/basic.md new file mode 100644 index 0000000000..5fb2e9787b --- /dev/null +++ b/components/typography/demo/basic.md @@ -0,0 +1,14 @@ +--- +order: 0 +title: + zh-CN: 基本 + en-US: Basic +--- + +## zh-CN + +展示文档样例。 + +## en-US + +Display the document sample. diff --git a/components/typography/demo/basic.ts b/components/typography/demo/basic.ts new file mode 100644 index 0000000000..da18066081 --- /dev/null +++ b/components/typography/demo/basic.ts @@ -0,0 +1,83 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-typography-basic', + template: ` +
+

Introduction

+

+ 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. +

+

+ After massive project practice and summaries, Ant Design, a design language for backgroundapplications, is + refined by Ant UED Team, which aims to + uniform the user interface specs for internal background projects, lower the unnecessary cost of design + differences and implementation and liberate the resources ofdesign and front-end development. +

+

Guidelines and Resources

+

+ We supply a series of design principles, practical patterns and high quality design resources (Sketch + and Axure), to help people create their product prototypes beautifully and efficiently. +

+ + + + +

介绍

+

+ 蚂蚁的企业级产品是一个庞大且复杂的体系。这类产品不仅量级巨大且功能复杂,而且变动和并发频繁,常常需要设计与开发能够快速的做出响应。 + 同时这类产品中有存在很多类似的页面以及组件,可以通过抽象得到一些稳定且高复用性的内容。 +

+

+ 随着商业化的趋势,越来越多的企业级产品对更好的用户体验有了进一步的要求。带着这样的一个终极目标,我们(蚂蚁金服体验技术部) + 经过大量的项目实践和总结,逐步打磨出一个服务于企业级产品的设计体系 Ant Design。 基于『确定』和『自然』的设计价值观,通过模块化的解决方案,降低冗余的生产成本, 让设计者专注于更好的用户体验。 +

+

+ 设计资源 +

+

+ 我们提供完善的设计原则、最佳实践和设计资源文件 (Sketch 和 + Axure),来帮助业务快速设计出高质 量的产品原型。 +

+ + + +
+ `, + styles: [] +}) +export class NzDemoTypographyBasicComponent {} diff --git a/components/typography/demo/ellipsis.md b/components/typography/demo/ellipsis.md new file mode 100644 index 0000000000..e0b59175f8 --- /dev/null +++ b/components/typography/demo/ellipsis.md @@ -0,0 +1,14 @@ +--- +order: 3 +title: + zh-CN: 省略号 + en-US: ellipsis +--- + +## zh-CN + +多行文本省略。 + +## en-US + +Multiple line ellipsis support. diff --git a/components/typography/demo/ellipsis.ts b/components/typography/demo/ellipsis.ts new file mode 100644 index 0000000000..db259ecaa4 --- /dev/null +++ b/components/typography/demo/ellipsis.ts @@ -0,0 +1,41 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-typography-ellipsis', + template: ` +

+ 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. +

+
+

+ 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. +

+
+

+ `, + styles: [] +}) +export class NzDemoTypographyEllipsisComponent { + dynamicContent = `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.`; + + onChange(event: string): void { + this.dynamicContent = event; + } +} diff --git a/components/typography/demo/interactive.md b/components/typography/demo/interactive.md new file mode 100644 index 0000000000..2e3aee8898 --- /dev/null +++ b/components/typography/demo/interactive.md @@ -0,0 +1,14 @@ +--- +order: 2 +title: + zh-CN: 可交互 + en-US: Interactive +--- + +## zh-CN + +提供额外的交互能力。 + +## en-US + +Provide additional interactive capacity. diff --git a/components/typography/demo/interactive.ts b/components/typography/demo/interactive.ts new file mode 100644 index 0000000000..716c0bf646 --- /dev/null +++ b/components/typography/demo/interactive.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-typography-interactive', + template: ` +

+

+

Replace copy text.

+ `, + styles: [] +}) +export class NzDemoTypographyInteractiveComponent { + str = 'This is an editable text.'; +} diff --git a/components/typography/demo/text.md b/components/typography/demo/text.md new file mode 100644 index 0000000000..d7b145f18e --- /dev/null +++ b/components/typography/demo/text.md @@ -0,0 +1,14 @@ +--- +order: 2 +title: + zh-CN: 文本组件 + en-US: Text Component +--- + +## zh-CN + +内置不同样式的文本。 + +## en-US + +Provides multiple types of text. diff --git a/components/typography/demo/text.ts b/components/typography/demo/text.ts new file mode 100644 index 0000000000..3a14708c38 --- /dev/null +++ b/components/typography/demo/text.ts @@ -0,0 +1,30 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-typography-text', + template: ` + Ant Design +
+ Ant Design +
+ Ant Design +
+ Ant Design +
+ Ant Design +
+ Ant Design +
+ Ant Design +
+ Ant Design +
+ Ant Design +
+ Ant Design + `, + styles : [] +}) +export class NzDemoTypographyTextComponent { + +} diff --git a/components/typography/demo/title.md b/components/typography/demo/title.md new file mode 100644 index 0000000000..1565966d15 --- /dev/null +++ b/components/typography/demo/title.md @@ -0,0 +1,14 @@ +--- +order: 1 +title: + zh-CN: 标题组件 + en-US: Title Component +--- + +## zh-CN + +展示不同级别的标题。 + +## en-US + +Display title in different level. diff --git a/components/typography/demo/title.ts b/components/typography/demo/title.ts new file mode 100644 index 0000000000..bf807657fd --- /dev/null +++ b/components/typography/demo/title.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-typography-title', + template: ` +

h1. Ant Design

+

h2. Ant Design

+

h3. Ant Design

+

h4. Ant Design

+ `, + styles : [] +}) +export class NzDemoTypographyTitleComponent { + +} diff --git a/components/typography/doc/index.en-US.md b/components/typography/doc/index.en-US.md new file mode 100644 index 0000000000..554ea8a824 --- /dev/null +++ b/components/typography/doc/index.en-US.md @@ -0,0 +1,31 @@ +--- +category: Components +type: General +title: Typography +cols: 1 +--- + +Basic text writing, including headings, body text, lists, and more. + +## When To Use + +- When need to display title or paragraph contents in Articles/Blogs/Notes. +- When you need copyable/editable/ellipsis texts. + +## API + +### nz-typography + +| Property | Description | Type | Default | +| -------- | ----------- | ---- | ------- | +| `[nzContent]` | Component content | `string` | - | +| `[nzCopyable]` | Can copy, require use `[nzContent]` | `boolean` | `false` | +| `[nzEditable]` | Editable, require use `[nzContent]` | `boolean` | `false` | +| `[nzEllipsis]` | Display ellipsis when overflow, require use `[nzContent]` when dynamic content | `boolean` | `false` | +| `[nzCopyText]` | Customize the copy text | `string` | - | +| `[nzDisabled]` | Disable content | `boolean` | `false` | +| `[nzExpandable]` | Expandable when ellipsis | `boolean` | `false` | +| `[nzEllipsisRows]` | Line number | `number` | `1` | +| `[nzType]` | Content type | `'secondary'|'warning'|'danger'` | - | +| `(nzContentChange)` | Trigger when user edit the content | `EventEmitter` | - | +| `(nzExpandChange)` | Trigger when user expanded the content | `EventEmitter` | - | \ No newline at end of file diff --git a/components/typography/doc/index.zh-CN.md b/components/typography/doc/index.zh-CN.md new file mode 100644 index 0000000000..6524db24c1 --- /dev/null +++ b/components/typography/doc/index.zh-CN.md @@ -0,0 +1,31 @@ +--- +category: Components +subtitle: 排版 +type: 通用 +title: Typography +cols: 1 +--- +文本的基本格式。 + +## 何时使用 + +- 当需要展示标题、段落、列表内容时使用,如文章/博客/日志的文本样式。 +- 当需要一列基于文本的基础操作时,如拷贝/省略/可编辑。 + +## API + +### nz-typography + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| `[nzContent]` | 组件内容 | `string` | - | +| `[nzCopyable]` | 是否可拷贝,需要配合 `[nzContent]` 使用 | `boolean` | `false` | +| `[nzEditable]` | 是否可编辑,是否可拷贝,需要配合 `[nzContent]` 使用 | `boolean` | `false` | +| `[nzEllipsis]` | 自动溢出省略,动态内容时需要配合 `[nzContent]` 使用 | `boolean` | `false` | +| `[nzExpandable]` | 自动溢出省略时是否可展开 | `boolean` | `false` | +| `[nzCopyText]` | 自定义被拷贝的文本 | `string` | - | +| `[nzDisabled]` | 禁用文本 | `boolean` | `false` | +| `[nzEllipsisRows]` | 自动溢出省略时省略行数 | `number` | `1` | +| `[nzType]` | 文本类型 | `'secondary'|'warning'|'danger'` | - | +| `(nzContentChange)` | 当用户提交编辑内容时触发 | `EventEmitter` | - | +| `(nzExpandChange)` | 展开省略文本时触发 | `EventEmitter` | - | diff --git a/components/typography/index.ts b/components/typography/index.ts new file mode 100644 index 0000000000..f17e95188c --- /dev/null +++ b/components/typography/index.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +export * from './public-api'; diff --git a/components/typography/nz-text-copy.component.html b/components/typography/nz-text-copy.component.html new file mode 100644 index 0000000000..c127107535 --- /dev/null +++ b/components/typography/nz-text-copy.component.html @@ -0,0 +1,11 @@ + + + + diff --git a/components/typography/nz-text-copy.component.ts b/components/typography/nz-text-copy.component.ts new file mode 100644 index 0000000000..ceb1fd37c0 --- /dev/null +++ b/components/typography/nz-text-copy.component.ts @@ -0,0 +1,84 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, + ViewEncapsulation +} from '@angular/core'; + +import { NzCopyToClipboardService } from 'ng-zorro-antd/core'; +import { NzI18nService } from 'ng-zorro-antd/i18n'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +@Component({ + selector: 'nz-text-copy', + templateUrl: './nz-text-copy.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + preserveWhitespaces: false +}) +export class NzTextCopyComponent implements OnInit, OnDestroy { + copied = false; + copyId: number; + // tslint:disable-next-line:no-any + locale: any = {}; + nativeElement = this.host.nativeElement; + private destroy$ = new Subject(); + + @Input() text: string; + @Output() readonly textCopy = new EventEmitter(); + + constructor( + private host: ElementRef, + private cdr: ChangeDetectorRef, + private copyToClipboard: NzCopyToClipboardService, + private i18n: NzI18nService + ) {} + + ngOnInit(): void { + this.i18n.localeChange.pipe(takeUntil(this.destroy$)).subscribe(() => { + this.locale = this.i18n.getLocaleData('Text'); + this.cdr.markForCheck(); + }); + } + + ngOnDestroy(): void { + clearTimeout(this.copyId); + } + + onClick(): void { + if (this.copied) { + return; + } + this.copied = true; + this.cdr.detectChanges(); + const text = this.text; + this.textCopy.emit(text); + this.copyToClipboard + .copy(text) + .then(() => this.onCopied()) + .catch(() => this.onCopied()); + } + + onCopied(): void { + clearTimeout(this.copyId); + this.copyId = setTimeout(() => { + this.copied = false; + this.cdr.detectChanges(); + }, 3000); + } +} diff --git a/components/typography/nz-text-edit.component.html b/components/typography/nz-text-edit.component.html new file mode 100644 index 0000000000..f583db9b0e --- /dev/null +++ b/components/typography/nz-text-edit.component.html @@ -0,0 +1,22 @@ + + + + + + + diff --git a/components/typography/nz-text-edit.component.ts b/components/typography/nz-text-edit.component.ts new file mode 100644 index 0000000000..91719a04eb --- /dev/null +++ b/components/typography/nz-text-edit.component.ts @@ -0,0 +1,103 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, + ViewChild, + ViewEncapsulation +} from '@angular/core'; + +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +import { NzI18nService } from 'ng-zorro-antd/i18n'; +import { NzAutosizeDirective } from 'ng-zorro-antd/input'; + +@Component({ + selector: 'nz-text-edit', + templateUrl: './nz-text-edit.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + preserveWhitespaces: false +}) +export class NzTextEditComponent implements OnInit, OnDestroy { + editing = false; + // tslint:disable-next-line:no-any + locale: any = {}; + private destroy$ = new Subject(); + + @Input() text: string; + @Output() readonly startEditing = new EventEmitter(); + @Output() readonly endEditing = new EventEmitter(); + @ViewChild('textarea', { static: false }) textarea: ElementRef; + @ViewChild(NzAutosizeDirective, { static: false }) autosizeDirective: NzAutosizeDirective; + + beforeText: string; + currentText: string; + nativeElement = this.host.nativeElement; + constructor(private host: ElementRef, private cdr: ChangeDetectorRef, private i18n: NzI18nService) {} + + ngOnInit(): void { + this.i18n.localeChange.pipe(takeUntil(this.destroy$)).subscribe(() => { + this.locale = this.i18n.getLocaleData('Text'); + this.cdr.markForCheck(); + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + onClick(): void { + this.beforeText = this.text; + this.currentText = this.beforeText; + this.editing = true; + this.startEditing.emit(); + this.focusAndSetValue(); + } + + confirm(): void { + this.editing = false; + this.endEditing.emit(this.currentText); + } + + onInput(event: Event): void { + const target = event.target as HTMLTextAreaElement; + this.currentText = target.value; + } + + onEnter(event: KeyboardEvent): void { + event.stopPropagation(); + event.preventDefault(); + this.confirm(); + } + + onCancel(): void { + this.currentText = this.beforeText; + this.confirm(); + } + + focusAndSetValue(): void { + setTimeout(() => { + if (this.textarea && this.textarea.nativeElement) { + this.textarea.nativeElement.focus(); + this.textarea.nativeElement.value = this.currentText; + this.autosizeDirective.resizeToFitContent(); + } + }); + } +} diff --git a/components/typography/nz-typography.component.html b/components/typography/nz-typography.component.html new file mode 100644 index 0000000000..ba86418616 --- /dev/null +++ b/components/typography/nz-typography.component.html @@ -0,0 +1,24 @@ + + + {{content}} + + + + + + + + + {{ellipsisStr}} + {{locale?.expand}} + + + + + + + diff --git a/components/typography/nz-typography.component.ts b/components/typography/nz-typography.component.ts new file mode 100644 index 0000000000..863bdd305f --- /dev/null +++ b/components/typography/nz-typography.component.ts @@ -0,0 +1,249 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { Platform } from '@angular/cdk/platform'; +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EmbeddedViewRef, + EventEmitter, + Input, + NgZone, + OnChanges, + OnDestroy, + OnInit, + Output, + Renderer2, + SimpleChanges, + TemplateRef, + ViewChild, + ViewContainerRef, + ViewEncapsulation +} from '@angular/core'; + +import { fromEvent, Subject, Subscription } from 'rxjs'; +import { auditTime, takeUntil } from 'rxjs/operators'; + +import { cancelRequestAnimationFrame, isStyleSupport, measure, reqAnimFrame, InputBoolean } from 'ng-zorro-antd/core'; +import { NzI18nService } from 'ng-zorro-antd/i18n'; + +import { NzTextCopyComponent } from './nz-text-copy.component'; +import { NzTextEditComponent } from './nz-text-edit.component'; + +@Component({ + selector: ` + nz-typography, + [nz-typography], + p[nz-paragraph], + span[nz-text], + h1[nz-title], h2[nz-title], h3[nz-title], h4[nz-title] + `, + templateUrl: './nz-typography.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + preserveWhitespaces: false, + host: { + '[class.ant-typography]': '!editing', + '[class.ant-typography-edit-content]': 'editing', + '[class.ant-typography-secondary]': 'nzType === "secondary"', + '[class.ant-typography-warning]': 'nzType === "warning"', + '[class.ant-typography-danger]': 'nzType === "danger"', + '[class.ant-typography-disabled]': 'nzDisabled', + '[class.ant-typography-ellipsis]': 'nzEllipsis && !expanded', + '[class.ant-typography-ellipsis-single-line]': 'canCssEllipsis && nzEllipsisRows === 1', + '[class.ant-typography-ellipsis-multiple-line]': 'canCssEllipsis && nzEllipsisRows > 1', + '[style.-webkit-line-clamp]': '(canCssEllipsis && nzEllipsisRows > 1) ? nzEllipsisRows : null' + } +}) +export class NzTypographyComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges { + @Input() @InputBoolean() nzCopyable = false; + @Input() @InputBoolean() nzEditable = false; + @Input() @InputBoolean() nzDisabled = false; + @Input() @InputBoolean() nzExpandable = false; + @Input() @InputBoolean() nzEllipsis = false; + @Input() nzContent: string; + @Input() nzEllipsisRows = 1; + @Input() nzType: 'secondary' | 'warning' | 'danger' | undefined; + @Input() nzCopyText: string | undefined; + @Output() readonly nzContentChange = new EventEmitter(); + @Output() readonly nzCopy = new EventEmitter(); + @Output() readonly nzExpandChange = new EventEmitter(); + + @ViewChild(NzTextEditComponent, { static: false }) textEditRef: NzTextEditComponent; + @ViewChild(NzTextCopyComponent, { static: false }) textCopyRef: NzTextCopyComponent; + @ViewChild('ellipsisContainer', { static: false }) ellipsisContainer: ElementRef; + @ViewChild('expandable', { static: false }) expandableBtn: ElementRef; + @ViewChild('contentTemplate', { static: false }) contentTemplate: TemplateRef<{ content: string }>; + + // tslint:disable-next-line:no-any + locale: any = {}; + editing = false; + ellipsisText: string | undefined; + cssEllipsis: boolean = false; + isEllipsis: boolean = false; + expanded: boolean = false; + ellipsisStr = '...'; + + get canCssEllipsis(): boolean { + return this.nzEllipsis && this.cssEllipsis && !this.expanded; + } + + private viewInit = false; + private rfaId: number = -1; + private destroy$ = new Subject(); + private windowResizeSubscription = Subscription.EMPTY; + get copyText(): string { + return typeof this.nzCopyText === 'string' ? this.nzCopyText : this.nzContent; + } + + constructor( + private host: ElementRef, + private cdr: ChangeDetectorRef, + private viewContainerRef: ViewContainerRef, + private renderer: Renderer2, + private ngZone: NgZone, + private platform: Platform, + private i18n: NzI18nService + ) {} + + onTextCopy(text: string): void { + this.nzCopy.emit(text); + } + + onStartEditing(): void { + this.editing = true; + } + + onEndEditing(text: string): void { + this.editing = false; + this.nzContentChange.emit(text); + this.resizeOnNextFrameIfNeed(); + } + + onExpand(): void { + this.expanded = true; + this.nzExpandChange.emit(); + } + + canUseCSSEllipsis(): boolean { + if (this.nzEditable || this.nzCopyable || this.nzExpandable) { + return false; + } + if (this.nzEllipsisRows === 1) { + return isStyleSupport('textOverflow'); + } else { + return isStyleSupport('webkitLineClamp'); + } + } + + resizeOnNextFrameIfNeed(): void { + cancelRequestAnimationFrame(this.rfaId); + if (!this.viewInit || !this.nzEllipsis || this.nzEllipsisRows < 0 || this.expanded || !this.platform.isBrowser) { + return; + } + this.rfaId = reqAnimFrame(() => { + this.syncEllipsis(); + }); + } + + getOriginContentViewRef(): { viewRef: EmbeddedViewRef<{ content: string }>; removeView(): void } { + const viewRef = this.viewContainerRef.createEmbeddedView<{ content: string }>(this.contentTemplate, { + content: this.nzContent + }); + viewRef.detectChanges(); + return { + viewRef, + removeView: () => { + this.viewContainerRef.remove(this.viewContainerRef.indexOf(viewRef)); + } + }; + } + + syncEllipsis(): void { + if (this.cssEllipsis) { + return; + } + const { viewRef, removeView } = this.getOriginContentViewRef(); + const fixedNodes = [this.textCopyRef, this.textEditRef, this.expandableBtn] + .filter(e => e && e.nativeElement) + .map(e => e.nativeElement); + + const { contentNodes, text, ellipsis } = measure( + this.host.nativeElement, + this.nzEllipsisRows, + viewRef.rootNodes, + fixedNodes, + this.ellipsisStr + ); + + removeView(); + + if (this.ellipsisText !== text || this.isEllipsis !== ellipsis) { + this.ellipsisText = text; + this.isEllipsis = ellipsis; + const ellipsisContainerNativeElement = this.ellipsisContainer.nativeElement; + while (ellipsisContainerNativeElement.firstChild) { + this.renderer.removeChild(ellipsisContainerNativeElement, ellipsisContainerNativeElement.firstChild); + } + contentNodes.forEach(n => { + this.renderer.appendChild(ellipsisContainerNativeElement, n.cloneNode(true)); + }); + this.cdr.markForCheck(); + } + } + + private resizeAndSubscribeWindowResize(): void { + if (this.platform.isBrowser) { + this.windowResizeSubscription.unsubscribe(); + this.cssEllipsis = this.canUseCSSEllipsis(); + this.resizeOnNextFrameIfNeed(); + this.ngZone.runOutsideAngular(() => { + this.windowResizeSubscription = fromEvent(window, 'resize') + .pipe( + auditTime(16), + takeUntil(this.destroy$) + ) + .subscribe(() => this.resizeOnNextFrameIfNeed()); + }); + } + } + + ngOnInit(): void { + this.i18n.localeChange.pipe(takeUntil(this.destroy$)).subscribe(() => { + this.locale = this.i18n.getLocaleData('Text'); + this.cdr.markForCheck(); + }); + } + + ngAfterViewInit(): void { + this.viewInit = true; + this.resizeAndSubscribeWindowResize(); + } + + ngOnChanges(changes: SimpleChanges): void { + const { nzCopyable, nzEditable, nzExpandable, nzEllipsis, nzContent, nzEllipsisRows } = changes; + if (nzCopyable || nzEditable || nzExpandable || nzEllipsis || nzContent || nzEllipsisRows) { + if (this.nzEllipsis) { + if (this.expanded) { + this.windowResizeSubscription.unsubscribe(); + } else { + this.resizeAndSubscribeWindowResize(); + } + } + } + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + this.windowResizeSubscription.unsubscribe(); + } +} diff --git a/components/typography/nz-typography.module.ts b/components/typography/nz-typography.module.ts new file mode 100644 index 0000000000..217ef2472d --- /dev/null +++ b/components/typography/nz-typography.module.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { NzTransButtonDirective } from 'ng-zorro-antd/core'; +import { NzI18nModule } from 'ng-zorro-antd/i18n'; +import { NzIconModule } from 'ng-zorro-antd/icon'; +import { NzInputModule } from 'ng-zorro-antd/input'; +import { NzToolTipModule } from 'ng-zorro-antd/tooltip'; + +import { NzTextCopyComponent } from './nz-text-copy.component'; +import { NzTextEditComponent } from './nz-text-edit.component'; +import { NzTypographyComponent } from './nz-typography.component'; + +@NgModule({ + imports: [CommonModule, NzIconModule, NzToolTipModule, NzInputModule, NzI18nModule], + exports: [NzTypographyComponent, NzTextCopyComponent, NzTextEditComponent, NzTransButtonDirective], + declarations: [NzTypographyComponent, NzTextCopyComponent, NzTextEditComponent, NzTransButtonDirective] +}) +export class NzTypographyModule {} diff --git a/components/typography/nz-typography.spec.ts b/components/typography/nz-typography.spec.ts new file mode 100644 index 0000000000..d9cd32d512 --- /dev/null +++ b/components/typography/nz-typography.spec.ts @@ -0,0 +1,398 @@ +import { ENTER } from '@angular/cdk/keycodes'; +import { CommonModule } from '@angular/common'; +import { Component, ViewChild } from '@angular/core'; +import { fakeAsync, flush, tick, ComponentFixture, TestBed } from '@angular/core/testing'; +import { createKeyboardEvent, dispatchFakeEvent, typeInElement } from 'ng-zorro-antd/core'; +import { NzIconTestModule } from 'ng-zorro-antd/icon/testing'; + +import { NzTypographyComponent } from './nz-typography.component'; +import { NzTypographyModule } from './nz-typography.module'; + +// tslint:disable-next-line no-any +declare const viewport: any; + +describe('typography', () => { + let componentElement: HTMLElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [CommonModule, NzTypographyModule, NzIconTestModule], + declarations: [ + NzTestTypographyComponent, + NzTestTypographyCopyComponent, + NzTestTypographyEditComponent, + NzTestTypographyEllipsisComponent + ] + }).compileComponents(); + }); + + describe('base', () => { + let fixture: ComponentFixture; + + beforeEach(() => { + fixture = TestBed.createComponent(NzTestTypographyComponent); + componentElement = fixture.debugElement.nativeElement; + fixture.detectChanges(); + }); + + it('should selector work', () => { + const elements = componentElement.querySelectorAll( + 'h1[nz-title],' + 'h2[nz-title],' + 'h3[nz-title],' + 'h4[nz-title],' + 'p[nz-paragraph],' + 'span[nz-text]' + ); + elements.forEach(el => { + expect(el.classList).toContain('ant-typography'); + }); + }); + + it('should [nzType] work', () => { + expect(componentElement.querySelector('.test-secondary')!.classList).toContain('ant-typography-secondary'); + + expect(componentElement.querySelector('.test-warning')!.classList).toContain('ant-typography-warning'); + + expect(componentElement.querySelector('.test-danger')!.classList).toContain('ant-typography-danger'); + }); + + it('should [nzDisabled] work', () => { + expect(componentElement.querySelector('.test-disabled')!.classList).toContain('ant-typography-disabled'); + }); + }); + + describe('copyable', () => { + let fixture: ComponentFixture; + let testComponent: NzTestTypographyCopyComponent; + + beforeEach(() => { + fixture = TestBed.createComponent(NzTestTypographyCopyComponent); + testComponent = fixture.componentInstance; + componentElement = fixture.debugElement.nativeElement; + fixture.detectChanges(); + }); + + it('should copyable', () => { + spyOn(testComponent, 'onCopy'); + const copyButtons = componentElement.querySelectorAll('.ant-typography-copy'); + expect(copyButtons.length).toBe(4); + copyButtons.forEach((btn, i) => { + btn.click(); + fixture.detectChanges(); + expect(testComponent.onCopy).toHaveBeenCalledWith(`Ant Design-${i}`); + }); + }); + + it('should only trigger once within 3000ms', fakeAsync(() => { + spyOn(testComponent, 'onCopy'); + const copyButton = componentElement.querySelector('.ant-typography-copy'); + expect(testComponent.onCopy).toHaveBeenCalledTimes(0); + copyButton!.click(); + fixture.detectChanges(); + copyButton!.click(); + fixture.detectChanges(); + expect(testComponent.onCopy).toHaveBeenCalledTimes(1); + tick(3000); + fixture.detectChanges(); + copyButton!.click(); + fixture.detectChanges(); + expect(testComponent.onCopy).toHaveBeenCalledTimes(2); + flush(); + fixture.detectChanges(); + })); + }); + + describe('editable', () => { + let fixture: ComponentFixture; + let testComponent: NzTestTypographyEditComponent; + + beforeEach(fakeAsync(() => { + fixture = TestBed.createComponent(NzTestTypographyEditComponent); + testComponent = fixture.componentInstance; + componentElement = fixture.debugElement.nativeElement; + fixture.detectChanges(); + })); + + afterEach(fakeAsync(() => { + flush(); + fixture.detectChanges(); + })); + + it('should discard changes when Esc keydown', () => { + const editButton = componentElement.querySelector('.ant-typography-edit'); + editButton!.click(); + fixture.detectChanges(); + expect(testComponent.str).toBe('This is an editable text.'); + const textarea = componentElement.querySelector('textarea')!; + typeInElement('test', textarea); + fixture.detectChanges(); + testComponent.nzTypographyComponent.textEditRef.onCancel(); + fixture.detectChanges(); + expect(testComponent.str).toBe('This is an editable text.'); + }); + + it('should edit work', () => { + const editButton = componentElement.querySelector('.ant-typography-edit'); + editButton!.click(); + fixture.detectChanges(); + expect(testComponent.str).toBe('This is an editable text.'); + const textarea = componentElement.querySelector('textarea')!; + typeInElement('test', textarea); + fixture.detectChanges(); + dispatchFakeEvent(textarea, 'blur'); + fixture.detectChanges(); + expect(testComponent.str).toBe('test'); + }); + + it('should edit focus', fakeAsync(() => { + const editButton = componentElement.querySelector('.ant-typography-edit'); + editButton!.click(); + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + const textarea = componentElement.querySelector('textarea')! as HTMLTextAreaElement; + expect(document.activeElement === textarea).toBe(true); + dispatchFakeEvent(textarea, 'blur'); + fixture.detectChanges(); + })); + + it('should apply changes when Enter keydown', () => { + const editButton = componentElement.querySelector('.ant-typography-edit'); + editButton!.click(); + fixture.detectChanges(); + const textarea = componentElement.querySelector('textarea')!; + typeInElement('test', textarea); + fixture.detectChanges(); + const event = createKeyboardEvent('keydown', ENTER, textarea); + testComponent.nzTypographyComponent.textEditRef.onEnter(event); + fixture.detectChanges(); + expect(testComponent.str).toBe('test'); + }); + }); + + describe('ellipsis', () => { + let fixture: ComponentFixture; + let testComponent: NzTestTypographyEllipsisComponent; + + beforeEach(fakeAsync(() => { + viewport.set(1200, 1000); + fixture = TestBed.createComponent(NzTestTypographyEllipsisComponent); + testComponent = fixture.componentInstance; + componentElement = fixture.debugElement.nativeElement; + fixture.detectChanges(); + tick(16); + fixture.detectChanges(); + })); + + it('should ellipsis work', fakeAsync(() => { + componentElement.querySelectorAll('p').forEach(e => { + expect(e.classList).toContain('ant-typography-ellipsis'); + }); + })); + + it('should css ellipsis', fakeAsync(() => { + const singleLine = componentElement.querySelector('.single')!; + const multipleLine = componentElement.querySelector('.multiple')!; + const dynamicContent = componentElement.querySelector('.dynamic')!; + expect(singleLine.classList).toContain('ant-typography-ellipsis-single-line'); + expect(multipleLine.classList).toContain('ant-typography-ellipsis-multiple-line'); + expect(dynamicContent.classList).toContain('ant-typography-ellipsis-multiple-line'); + testComponent.expandable = true; + fixture.detectChanges(); + expect(singleLine.classList).not.toContain('ant-typography-ellipsis-single-line'); + expect(multipleLine.classList).not.toContain('ant-typography-ellipsis-multiple-line'); + expect(dynamicContent.classList).not.toContain('ant-typography-ellipsis-multiple-line'); + })); + + it('should resize when content changed', fakeAsync(() => { + testComponent.expandable = true; + fixture.detectChanges(); + tick(16); + fixture.detectChanges(); + const dynamicContent = componentElement.querySelector('.dynamic')! as HTMLParagraphElement; + expect(dynamicContent.innerText.includes('...')).toBe(true); + testComponent.str = 'short content.'; + fixture.detectChanges(); + tick(16); + fixture.detectChanges(); + expect(dynamicContent.innerText.includes('...')).toBe(false); + })); + + it('should resize work', fakeAsync(() => { + testComponent.expandable = true; + viewport.set(400, 1000); + dispatchFakeEvent(window, 'resize'); + tick(16); + fixture.detectChanges(); + tick(32); + fixture.detectChanges(); + componentElement.querySelectorAll('p').forEach(e => { + expect(e.innerText.includes('...')).toBe(true); + }); + viewport.set(8000, 1000); + dispatchFakeEvent(window, 'resize'); + fixture.detectChanges(); + tick(32); + fixture.detectChanges(); + componentElement.querySelectorAll('p').forEach(e => { + expect(e.innerText.includes('...')).toBe(false); + }); + viewport.set(400, 1000); + dispatchFakeEvent(window, 'resize'); + fixture.detectChanges(); + tick(16); + viewport.set(800, 1000); + dispatchFakeEvent(window, 'resize'); + fixture.detectChanges(); + tick(32); + fixture.detectChanges(); + componentElement.querySelectorAll('p').forEach(e => { + expect(e.innerText.includes('...')).toBe(true); + }); + })); + + it('should expandable', fakeAsync(() => { + testComponent.expandable = true; + viewport.set(400, 1000); + dispatchFakeEvent(window, 'resize'); + fixture.detectChanges(); + tick(16); + fixture.detectChanges(); + tick(16); + componentElement.querySelectorAll('p').forEach((e, i) => { + expect(e.classList).toContain('ant-typography-ellipsis'); + const expandBtn = e.querySelector('.ant-typography-expand') as HTMLAnchorElement; + expect(expandBtn).toBeTruthy(); + expandBtn!.click(); + fixture.detectChanges(); + expect(e.classList).not.toContain('ant-typography-ellipsis'); + expect(testComponent.onExpand).toHaveBeenCalledTimes(i + 1); + }); + })); + + it('should not resize when is expanded', fakeAsync(() => { + testComponent.expandable = true; + viewport.set(400, 1000); + dispatchFakeEvent(window, 'resize'); + fixture.detectChanges(); + tick(16); + fixture.detectChanges(); + tick(16); + componentElement.querySelectorAll('p').forEach(e => { + const expandBtn = e.querySelector('.ant-typography-expand') as HTMLAnchorElement; + expandBtn!.click(); + fixture.detectChanges(); + }); + testComponent.expandable = false; + fixture.detectChanges(); + tick(16); + viewport.set(800, 1000); + dispatchFakeEvent(window, 'resize'); + fixture.detectChanges(); + tick(32); + fixture.detectChanges(); + componentElement.querySelectorAll('p').forEach(e => { + expect(e.innerText.includes('...')).toBe(false); + }); + })); + }); +}); + +@Component({ + selector: 'nz-test-typography', + template: ` +

h1. Ant Design

+

h2. Ant Design

+

h3. Ant Design

+

h4. Ant Design

+

Ant Design, a design language for background applications, is refined by Ant UED Team

+ Ant Design + Ant Design + Ant Design + Ant Design + Ant Design + Ant Design + Ant Design + Ant Design + Ant Design + Ant Design + ` +}) +export class NzTestTypographyComponent {} + +@Component({ + selector: 'nz-test-typography-copy', + template: ` +

+

+ + Test + ` +}) +export class NzTestTypographyCopyComponent { + onCopy(_text: string): void { + // noop + } +} + +@Component({ + selector: 'nz-test-typography-edit', + template: ` +

+ ` +}) +export class NzTestTypographyEditComponent { + @ViewChild(NzTypographyComponent, { static: false }) nzTypographyComponent: NzTypographyComponent; + str = 'This is an editable text.'; + onChange = (text: string): void => { + this.str = text; + }; +} + +@Component({ + template: ` +

+ 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. +

+
+

+ 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. +

+

+ `, + styles: [ + ` + p { + line-height: 1.5; + } + ` + ] +}) +export class NzTestTypographyEllipsisComponent { + expandable = false; + onExpand = jasmine.createSpy('expand callback'); + + @ViewChild(NzTypographyComponent, { static: false }) nzTypographyComponent: NzTypographyComponent; + str = new Array(5) + .fill('Ant Design, a design language for background applications, is refined by Ant UED Team.') + .join(''); +} diff --git a/components/typography/package.json b/components/typography/package.json new file mode 100644 index 0000000000..61f8a57dcc --- /dev/null +++ b/components/typography/package.json @@ -0,0 +1,7 @@ +{ + "ngPackage": { + "lib": { + "entryFile": "public-api.ts" + } + } +} \ No newline at end of file diff --git a/components/typography/public-api.ts b/components/typography/public-api.ts new file mode 100644 index 0000000000..8ee00bd1b5 --- /dev/null +++ b/components/typography/public-api.ts @@ -0,0 +1,12 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +export { NzTypographyModule } from './nz-typography.module'; +export { NzTypographyComponent } from './nz-typography.component'; +export { NzTextCopyComponent } from './nz-text-copy.component'; +export { NzTextEditComponent } from './nz-text-edit.component'; diff --git a/components/typography/style/entry.less b/components/typography/style/entry.less new file mode 100644 index 0000000000..f65fa7e86a --- /dev/null +++ b/components/typography/style/entry.less @@ -0,0 +1,5 @@ +@import './index.less'; + +// style dependencies +@import '../../tooltip/style/entry.less'; +@import '../../input/style/entry.less'; \ No newline at end of file diff --git a/components/typography/style/index.less b/components/typography/style/index.less new file mode 100644 index 0000000000..0940298540 --- /dev/null +++ b/components/typography/style/index.less @@ -0,0 +1,225 @@ +@import '../../style/themes/index'; +@import '../../style/mixins/index'; + +@typography-prefix-cls: ~'@{ant-prefix}-typography'; +@typography-title-margin-top: 1.2em; + +// =============== Common =============== +.typography-paragraph() { + margin-bottom: 1em; +} + +.typography-title(@fontSize; @lineHeight) { + margin-bottom: 0.5em; + color: @heading-color; + font-weight: 600; + font-size: @fontSize; + line-height: @lineHeight; +} + +.typography-title-1() { + .typography-title(@heading-1-size, 1.23); +} +.typography-title-2() { + .typography-title(@heading-2-size, 1.35); +} +.typography-title-3() { + .typography-title(@heading-3-size, 1.35); +} +.typography-title-4() { + .typography-title(@heading-4-size, 1.4); +} + +// =============== Basic =============== +.@{typography-prefix-cls} { + color: @text-color; + + &&-secondary { + color: @text-color-secondary; + } + + &&-warning { + color: @warning-color; + } + + &&-danger { + color: @error-color; + } + + &&-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(); + } + + h1&, + h2&, + h3&, + h4& { + .@{typography-prefix-cls} + & { + margin-top: @typography-title-margin-top; + } + } + + div, + ul, + li, + p, + h1, + h2, + h3, + h4 { + + h1, + + h2, + + h3, + + h4 { + margin-top: @typography-title-margin-top; + } + } + + span&-ellipsis { + display: inline-block; + } + + a { + .operation-unit(); + + &:active, + &:hover { + text-decoration: @link-hover-decoration; + } + + &[disabled] { + color: @disabled-color; + cursor: not-allowed; + pointer-events: none; + } + } + + code { + margin: 0 0.2em; + padding: 0.2em 0.4em 0.1em; + font-size: 85%; + background: rgba(0, 0, 0, 0.06); + border: 1px solid rgba(0, 0, 0, 0.06); + 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: 8px; + } + + &-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; + margin-bottom: calc(1em - @input-padding-vertical-base - 2px); + } + + &-confirm { + position: absolute; + right: 10px; + bottom: 8px; + color: @text-color-secondary; + pointer-events: none; + } + } + + // list + ul, + ol { + margin: 0 0 1em 0; + padding: 0; + + li { + margin: 0 0 0 20px; + padding: 0 0 0 4px; + } + } + + ul li { + list-style-type: circle; + + li { + list-style-type: disc; + } + } + + ol li { + list-style-type: decimal; + } + + // ============ Ellipsis ============ + &-ellipsis-single-line { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + &-ellipsis-multiple-line { + display: -webkit-box; + -webkit-line-clamp: 3; + /*! autoprefixer: ignore next */ + -webkit-box-orient: vertical; + overflow: hidden; + } +} \ No newline at end of file diff --git a/scripts/prerender/static.paths.ts b/scripts/prerender/static.paths.ts index 1abf82c30a..af44d5aa7f 100644 --- a/scripts/prerender/static.paths.ts +++ b/scripts/prerender/static.paths.ts @@ -114,6 +114,8 @@ export const ROUTES = [ '/components/tree-select/zh', '/components/tree/en', '/components/tree/zh', + '/components/typography/en', + '/components/typography/zh', '/components/upload/en', '/components/upload/zh', '/doc/introduce/en',