diff --git a/components/cascader/nz-cascader.component.ts b/components/cascader/nz-cascader.component.ts index 9b98cbd9274..83b6e945510 100644 --- a/components/cascader/nz-cascader.component.ts +++ b/components/cascader/nz-cascader.component.ts @@ -21,7 +21,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { slideMotion } from '../core/animation/slide'; import { DEFAULT_CASCADER_POSITIONS } from '../core/overlay/overlay-position'; import { NgClassType } from '../core/types/ng-class'; -import { arrayEquals, toArray } from '../core/util/array'; +import { arraysEqual, toArray } from '../core/util/array'; import { InputBoolean } from '../core/util/convert'; import { @@ -355,7 +355,7 @@ export class NzCascaderComponent implements OnDestroy, ControlValueAccessor { } private setColumnData(options: CascaderOption[], columnIndex: number): void { - if (!arrayEquals(this.columns[ columnIndex ], options)) { + if (!arraysEqual(this.columns[ columnIndex ], options)) { this.columns[ columnIndex ] = options; if (columnIndex < this.columns.length - 1) { this.columns = this.columns.slice(0, columnIndex + 1); @@ -390,7 +390,7 @@ export class NzCascaderComponent implements OnDestroy, ControlValueAccessor { private onValueChange(): void { const value = this.getSubmitValue(); - if (!arrayEquals(this.value, value)) { + if (!arraysEqual(this.value, value)) { this.defaultValue = null; this.value = value; this.onChange(value); @@ -410,7 +410,7 @@ export class NzCascaderComponent implements OnDestroy, ControlValueAccessor { //#endregion - //#region Mouse and keyboard event handlers, view children + //#region Mouse and keyboard event handles, view children focus(): void { if (!this.isFocused) { diff --git a/components/core/dom/reverse.ts b/components/core/dom/reverse.ts deleted file mode 100644 index 5a739f9d4a4..00000000000 --- a/components/core/dom/reverse.ts +++ /dev/null @@ -1,11 +0,0 @@ -export function reverseChildNodes(parent: HTMLElement): void { - const children = parent.childNodes; - let length = children.length; - if (length) { - const nodes: Node[] = []; - children.forEach((node, i) => nodes[ i ] = node); - while (length--) { - parent.appendChild(nodes[ length ]); - } - } -} diff --git a/components/core/style/map.ts b/components/core/style/map.ts deleted file mode 100644 index a344f42a3d5..00000000000 --- a/components/core/style/map.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function classMapToString(map: { [ key: string ]: boolean }): string { - return Object.keys(map).filter(item => !!map[ item ]).join(' '); -} diff --git a/components/core/util/array.ts b/components/core/util/array.ts index a05e9ffff17..1fb25327264 100644 --- a/components/core/util/array.ts +++ b/components/core/util/array.ts @@ -10,7 +10,7 @@ export function toArray(value: T | T[]): T[] { return ret; } -export function arrayEquals(array1: T[], array2: T[]): boolean { +export function arraysEqual(array1: T[], array2: T[]): boolean { if (!array1 || !array2 || array1.length !== array2.length) { return false; } @@ -23,3 +23,7 @@ export function arrayEquals(array1: T[], array2: T[]): boolean { } return true; } + +export function shallowCopyArray(source: T[]): T[] { + return source.slice(); +} diff --git a/components/core/util/check.ts b/components/core/util/check.ts index 79ffa21dfdb..02c71003169 100644 --- a/components/core/util/check.ts +++ b/components/core/util/check.ts @@ -5,7 +5,9 @@ export function isNotNil(value: any): boolean { return (typeof(value) !== 'undefined') && value !== null; } -/** 校验对象是否相等 */ +/** + * Examine if two objects are shallowly equaled. + */ export function shallowEqual(objA: {}, objB: {}): boolean { if (objA === objB) { return true; diff --git a/components/core/util/dom.ts b/components/core/util/dom.ts index e2e5c6f8f42..61f486f5840 100644 --- a/components/core/util/dom.ts +++ b/components/core/util/dom.ts @@ -1,5 +1,28 @@ +import { Observable } from 'rxjs'; + import { filterNotEmptyNode } from './check'; +/** + * Silent an event by stopping and preventing it. + */ +export function silentEvent(e: Event): void { + e.stopPropagation(); + e.preventDefault(); +} + +export function getElementOffset(elem: HTMLElement): { top: number, left: number } { + if (!elem.getClientRects().length) { + return { top: 0, left: 0 }; + } + + const rect = elem.getBoundingClientRect(); + const win = elem.ownerDocument.defaultView; + return { + top : rect.top + win.pageYOffset, + left: rect.left + win.pageXOffset + }; +} + export function findFirstNotEmptyNode(element: HTMLElement): Node { const children = element.childNodes; for (let i = 0; i < children.length; i++) { @@ -20,4 +43,29 @@ export function findLastNotEmptyNode(element: HTMLElement): Node { } } return null; -} \ No newline at end of file +} + +export function reverseChildNodes(parent: HTMLElement): void { + const children = parent.childNodes; + let length = children.length; + if (length) { + const nodes: Node[] = []; + children.forEach((node, i) => nodes[ i ] = node); + while (length--) { + parent.appendChild(nodes[ length ]); + } + } +} + +export interface MouseTouchObserverConfig { + end: string; + move: string; + pluckKey: string[]; + start: string; + + end$?: Observable; + moveResolved$?: Observable; + startPlucked$?: Observable; + + filter?(e: Event): boolean; +} diff --git a/components/core/util/getMentions.ts b/components/core/util/getMentions.ts index d609bca212c..05f322cc389 100644 --- a/components/core/util/getMentions.ts +++ b/components/core/util/getMentions.ts @@ -1,4 +1,3 @@ - export function getRegExp(prefix: string | string[]): RegExp { const prefixArray = Array.isArray(prefix) ? prefix : [prefix]; let prefixToken = prefixArray.join('').replace(/(\$|\^)/g, '\\$1'); diff --git a/components/core/util/number.ts b/components/core/util/number.ts new file mode 100644 index 00000000000..946f97d832f --- /dev/null +++ b/components/core/util/number.ts @@ -0,0 +1,19 @@ +export function getPercent(min: number, max: number, value: number): number { + return (value - min) / (max - min) * 100; +} + +export function getPrecision(num: number): number { + const numStr = num.toString(); + const dotIndex = numStr.indexOf('.'); + return dotIndex >= 0 ? numStr.length - dotIndex - 1 : 0; +} + +export function ensureNumberInRange(num: number, min: number, max: number): number { + if (isNaN(num) || num < min) { + return min; + } else if (num > max) { + return max; + } else { + return num; + } +} diff --git a/components/slider/demo/basic.md b/components/slider/demo/basic.md index b2d08da0c90..e6886656309 100644 --- a/components/slider/demo/basic.md +++ b/components/slider/demo/basic.md @@ -7,13 +7,11 @@ title: ## zh-CN -基本滑动条。当 `range` 为 `true` 时,渲染为双滑块。当 `disabled` 为 `true` 时,滑块处于不可用状态。 +基本滑动条。当 `nzRange` 为 `true` 时,渲染为双滑块。当 `nzDisabled` 为 `true` 时,滑块处于不可用状态。 ## en-US -Basic slider. When `range` is `true`, display as dual thumb mode. When `disable` is `true`, the slider will not be interactable. +Basic slider. When `nzRange` is `true`, display as dual thumb mode. When `nzDisabled` is `true`, the slider will not be interactive. + - diff --git a/components/slider/demo/event.md b/components/slider/demo/event.md index c9d127b26b5..e877ac201f9 100644 --- a/components/slider/demo/event.md +++ b/components/slider/demo/event.md @@ -7,9 +7,8 @@ title: ## zh-CN -当 Slider 的值发生改变时,会触发 `onChange` 事件,并把改变后的值作为参数传入。在 `onmouseup` 时,会触发 `onAfterChange` 事件,并把当前值作为参数传入。 +当 Slider 的值发生改变时,会触发 `nzOnChange` 事件,并把改变后的值作为参数传入。在 `onmouseup` 时,会触发 `nzOnAfterChange` 事件,并把当前值作为参数传入。 ## en-US -The `onChange` callback function will fire when the user changes the slider's value. -The `onAfterChange` callback function will fire when `onmouseup` fired. +The `nzOnChange` callback function will fire when the user changes the slider's value. The `nzOnAfterChange` callback function will fire when `onmouseup` fired. diff --git a/components/slider/demo/mark.md b/components/slider/demo/mark.md index 5ace891ffd4..19f65c62992 100644 --- a/components/slider/demo/mark.md +++ b/components/slider/demo/mark.md @@ -7,21 +7,10 @@ title: ## zh-CN -使用 `marks` 属性标注分段式滑块,使用 `value` / `defaultValue` 指定滑块位置。当 `included=false` 时,表明不同标记间为并列关系。当 `step=null` 时,Slider 的可选值仅有 `marks` 标出来的部分。 +使用 `nzMarks` 属性标注分段式滑块,使用 `nzValue` / `nzDefaultValue` 指定滑块位置。当 `nzIncluded = false` 时,表明不同标记间为并列关系。当 `nzStep = null` 时,Slider 的可选值仅有 `nzMarks` 标出来的部分。 ## en-US -Using `marks` property to mark a graduated slider, use `value` or `defaultValue` to specify the position of thumb. -When `included` is false, means that different thumbs are coordinative. -when `step` is null, users can only slide the thumbs onto marks. +Using `nzMarks` property to mark a graduated slider, use `nzValue` or `nzDefaultValue` to specify the position of thumb. When `nzIncluded` is false, means that different thumbs are coordinated. When `nzStep` is null, users can only slide the thumbs onto marks. - - diff --git a/components/slider/demo/tip-formatter.md b/components/slider/demo/tip-formatter.md index 7314d3344e1..41c37f895e1 100644 --- a/components/slider/demo/tip-formatter.md +++ b/components/slider/demo/tip-formatter.md @@ -7,10 +7,10 @@ title: ## zh-CN -使用 `tipFormatter` 可以格式化 `Tooltip` 的内容,设置 `tipFormatter={null}`,则隐藏 `Tooltip`。 +使用 `nzTipFormatter` 可以格式化 `Tooltip` 的内容,设置 `nzTipFormatter = null`,则隐藏 `Tooltip`。 ## en-US -Use `tipFormatter` to format content of `Toolip`. If `tipFormatter` is null, hide it. +Use `nzTipFormatter` to format content of `Toolip`. If `nzTipFormatter` is null, hide it. diff --git a/components/slider/demo/tooltip.md b/components/slider/demo/tooltip.md new file mode 100644 index 00000000000..5bd7ac21b35 --- /dev/null +++ b/components/slider/demo/tooltip.md @@ -0,0 +1,16 @@ +--- +order: 7 +title: + zh-CN: 控制 Tooltip 的显示 + en-US: Control visibility of Tooltip +--- + +## zh-CN + +当 `nzTooltipVisible` 为 `always` 时,将始终显示 ToolTip,为 `never` 时反之则始终不显示,即使在拖动、移入时也是如此。 + +## en-US + +When `nzTooltipVisible` is `always`, Tooltip will show always. And set to `never`, tooltip would never show, even when user is dragging or hovering. + + diff --git a/components/slider/demo/tooltip.ts b/components/slider/demo/tooltip.ts new file mode 100644 index 00000000000..926c1f937d6 --- /dev/null +++ b/components/slider/demo/tooltip.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-slider-tooltip', + template: ` + + + ` +}) +export class NzDemoSliderTooltipComponent { +} diff --git a/components/slider/doc/index.en-US.md b/components/slider/doc/index.en-US.md index d3050cb1a41..5d54ccc9eb9 100644 --- a/components/slider/doc/index.en-US.md +++ b/components/slider/doc/index.en-US.md @@ -28,5 +28,6 @@ To input a value in a range. | `[nzTipFormatter]` | Slider will pass its value to `tipFormatter`, and display its value in Tooltip, and hide Tooltip when return value is null. | `(value: number) => string` | - | | `[ngModel]` | The value of slider. When `range` is `false`, use `number`, otherwise, use `[number, number]` | `number|number[]` | - | | `[nzVertical]` | If true, the slider will be vertical. | `boolean` | `false` | +| `[nzTooltipVisible]` | When set to `always` tooltips are always displayed. When set to `never` they are never displayed | `'default'|'always'|'never'` | `default` | | `(nzOnAfterChange)` | Fire when `onmouseup` is fired. | `EventEmitter` | - | | `(ngModelChange)` | Callback function that is fired when the user changes the slider's value. | `EventEmitter` | - | diff --git a/components/slider/doc/index.zh-CN.md b/components/slider/doc/index.zh-CN.md index 9e9d109009b..d271c7f6fb4 100644 --- a/components/slider/doc/index.zh-CN.md +++ b/components/slider/doc/index.zh-CN.md @@ -29,5 +29,6 @@ title: Slider | `[nzTipFormatter]` | Slider 会把当前值传给 `nzTipFormatter`,并在 Tooltip 中显示 `nzTipFormatter` 的返回值,若为 null,则隐藏 Tooltip。 | `(value: number) => string` | - | | `[ngModel]` | 设置当前取值。当 `range` 为 `false` 时,使用 `number`,否则用 `[number, number]` | `number|number[]` | - | | `[nzVertical]` | 值为 `true` 时,Slider 为垂直方向 | `boolean` | `false` | +| `[nzTooltipVisible]` | 值为 `always` 时总是显示,值为 `never` 时在任何情况下都不显示 | `'default'|'always'|'never'` | `default` | | `(nzOnAfterChange)` | 与 `onmouseup` 触发时机一致,把当前值作为参数传入。 | `EventEmitter` | - | | `(ngModelChange)` | 当 Slider 的值发生改变时,会触发 ngModelChange 事件,并把改变后的值作为参数传入。 | `EventEmitter>` | - | diff --git a/components/slider/nz-slider-definitions.ts b/components/slider/nz-slider-definitions.ts new file mode 100644 index 00000000000..2c5c6d976ef --- /dev/null +++ b/components/slider/nz-slider-definitions.ts @@ -0,0 +1,58 @@ +export type Mark = string | MarkObj; + +export interface MarkObj { + style?: object; + label: string; +} + +export class Marks { + [ key: number ]: Mark; +} + +/** + * Processed steps that would be passed to sub components. + */ +export interface ExtendedMark { + value: number; + offset: number; + config: Mark; +} + +/** + * Marks that would be rendered. + */ +export interface DisplayedMark extends ExtendedMark { + active: boolean; + label: string; + style?: object; +} + +/** + * Steps that would be rendered. + */ +export interface DisplayedStep extends ExtendedMark { + active: boolean; + style?: object; +} + +export type SliderShowTooltip = 'always' | 'never' | 'default'; + +export type SliderValue = number[] | number; + +export interface SliderHandler { + offset: number; + value: number; + active: boolean; +} + +export function isValueARange(value: SliderValue): value is number[] { + if (value instanceof Array) { + return value.length === 2; + } else { + return false; + } +} + +export function isConfigAObject(config: Mark): config is MarkObj { + return config instanceof Object; +} diff --git a/components/slider/nz-slider-error.ts b/components/slider/nz-slider-error.ts new file mode 100644 index 00000000000..e8e7f558dc7 --- /dev/null +++ b/components/slider/nz-slider-error.ts @@ -0,0 +1,3 @@ +export function getValueTypeNotMatchError(): Error { + return new Error(`The "nzRange" can't match the "nzValue"'s type, please check these properties: "nzRange", "nzValue", "nzDefaultValue".`); +} diff --git a/components/slider/nz-slider-handle.component.html b/components/slider/nz-slider-handle.component.html index aa0f0c34e65..dd17b964fdb 100644 --- a/components/slider/nz-slider-handle.component.html +++ b/components/slider/nz-slider-handle.component.html @@ -1,4 +1,7 @@ - -
+ +
-
\ No newline at end of file +
diff --git a/components/slider/nz-slider-handle.component.ts b/components/slider/nz-slider-handle.component.ts index 75c905c724b..24a550c5ec0 100644 --- a/components/slider/nz-slider-handle.component.ts +++ b/components/slider/nz-slider-handle.component.ts @@ -1,78 +1,128 @@ -import { Component, HostListener, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; +import { + AfterViewInit, + ChangeDetectionStrategy, ChangeDetectorRef, + Component, + ElementRef, + Input, + NgZone, + OnChanges, + OnDestroy, + SimpleChanges, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { fromEvent, Subscription } from 'rxjs'; -import { toBoolean } from '../core/util/convert'; +import { InputBoolean } from '../core/util/convert'; import { NzToolTipComponent } from '../tooltip/nz-tooltip.component'; +import { SliderShowTooltip } from './nz-slider-definitions'; import { NzSliderComponent } from './nz-slider.component'; @Component({ + changeDetection : ChangeDetectionStrategy.OnPush, + encapsulation : ViewEncapsulation.None, selector : 'nz-slider-handle', preserveWhitespaces: false, - templateUrl : './nz-slider-handle.component.html' + templateUrl : './nz-slider-handle.component.html', + host : { + '(mouseenter)': 'enterHandle()', + '(mouseleave)': 'leaveHandle()' + } }) -export class NzSliderHandleComponent implements OnChanges { +export class NzSliderHandleComponent implements OnChanges, AfterViewInit, OnDestroy { + @ViewChild(NzToolTipComponent) tooltip: NzToolTipComponent; - // Static properties - @Input() nzClassName: string; @Input() nzVertical: string; @Input() nzOffset: number; - @Input() nzValue: number; // [For tooltip] - @Input() nzTipFormatter: (value: number) => string; // [For tooltip] - @Input() set nzActive(value: boolean) { // [For tooltip] - const show = toBoolean(value); - if (this.tooltip) { - if (show) { - this.tooltip.show(); + @Input() nzValue: number; + @Input() nzTooltipVisible: SliderShowTooltip = 'default'; + @Input() nzTipFormatter: (value: number) => string; + @Input() @InputBoolean() nzActive = false; + + tooltipTitle: string; + style: object = {}; + + private hovers_ = new Subscription(); + + constructor( + private sliderComponent: NzSliderComponent, + private ngZone: NgZone, + private el: ElementRef, + private cdr: ChangeDetectorRef + ) { + } + + ngOnChanges(changes: SimpleChanges): void { + const { nzOffset, nzValue, nzActive, nzTooltipVisible } = changes; + + if (nzOffset) { + this.updateStyle(); + } + if (nzValue) { + this.updateTooltipTitle(); + this.updateTooltipPosition(); + } + if (nzActive) { + if (nzActive.currentValue) { + this.toggleTooltip(true); } else { - this.tooltip.hide(); + this.toggleTooltip(false); } } + if (nzTooltipVisible && !nzTooltipVisible.isFirstChange() && this.tooltip) { + this.tooltip.show(); + } } - // Locals - @ViewChild('tooltip') tooltip: NzToolTipComponent; // [For tooltip] - tooltipTitle: string; // [For tooltip] - style: object = {}; + ngAfterViewInit(): void { + if (this.nzTooltipVisible === 'always' && this.tooltip) { + Promise.resolve().then(() => this.tooltip.show()); + } + } - constructor(private _slider: NzSliderComponent) { + ngOnDestroy(): void { + this.hovers_.unsubscribe(); } - ngOnChanges(changes: SimpleChanges): void { - if (changes.nzOffset) { - this._updateStyle(); - } - if (changes.nzValue) { - this._updateTooltipTitle(); // [For tooltip] - this._updateTooltipPosition(); // [For tooltip] + enterHandle = () => { + if (!this.sliderComponent.isDragging) { + this.toggleTooltip(true); + this.updateTooltipPosition(); + this.cdr.detectChanges(); } } - // Hover to toggle tooltip when not dragging - @HostListener('mouseenter', [ '$event' ]) - onMouseEnter($event: MouseEvent): void { - if (!this._slider.isDragging) { - this.nzActive = true; + leaveHandle = () => { + if (!this.sliderComponent.isDragging) { + this.toggleTooltip(false); + this.cdr.detectChanges(); } } - @HostListener('mouseleave', [ '$event' ]) - onMouseLeave($event: MouseEvent): void { - if (!this._slider.isDragging) { - this.nzActive = false; + private toggleTooltip(show: boolean): void { + if (this.nzTooltipVisible !== 'default' || !this.tooltip) { + return; + } + + if (show) { + this.tooltip.show(); + } else { + this.tooltip.hide(); } } - private _updateTooltipTitle(): void { // [For tooltip] + private updateTooltipTitle(): void { this.tooltipTitle = this.nzTipFormatter ? this.nzTipFormatter(this.nzValue) : `${this.nzValue}`; } - private _updateTooltipPosition(): void { // [For tooltip] + private updateTooltipPosition(): void { if (this.tooltip) { - setTimeout(() => this.tooltip.updatePosition(), 0); // MAY use ngAfterViewChecked? but this will be called so many times. + Promise.resolve().then(() => this.tooltip.updatePosition()); } } - private _updateStyle(): void { + private updateStyle(): void { this.style[ this.nzVertical ? 'bottom' : 'left' ] = `${this.nzOffset}%`; } } diff --git a/components/slider/nz-slider-marks.component.html b/components/slider/nz-slider-marks.component.html index 1473911167b..946b2a3f76a 100644 --- a/components/slider/nz-slider-marks.component.html +++ b/components/slider/nz-slider-marks.component.html @@ -1,3 +1,10 @@ -
- +
+ +
\ No newline at end of file diff --git a/components/slider/nz-slider-marks.component.ts b/components/slider/nz-slider-marks.component.ts index 7e455aea9bb..6be1f6ed5ed 100644 --- a/components/slider/nz-slider-marks.component.ts +++ b/components/slider/nz-slider-marks.component.ts @@ -1,131 +1,95 @@ -import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core'; -import { toBoolean } from '../core/util/convert'; +import { InputBoolean } from '../core/util/convert'; + +import { isConfigAObject, DisplayedMark, ExtendedMark, Mark } from './nz-slider-definitions'; @Component({ - selector : 'nz-slider-marks', + changeDetection : ChangeDetectionStrategy.OnPush, + encapsulation : ViewEncapsulation.None, preserveWhitespaces: false, + selector : 'nz-slider-marks', templateUrl : './nz-slider-marks.component.html' }) export class NzSliderMarksComponent implements OnChanges { - private _vertical = false; - private _included = false; - - // Dynamic properties @Input() nzLowerBound: number = null; @Input() nzUpperBound: number = null; - @Input() nzMarksArray: MarksArray; - - // Static properties - @Input() nzClassName: string; - @Input() nzMin: number; // Required - @Input() nzMax: number; // Required - - @Input() - set nzVertical(value: boolean) { // Required - this._vertical = toBoolean(value); - } - - get nzVertical(): boolean { - return this._vertical; - } - - @Input() - set nzIncluded(value: boolean) { - this._included = toBoolean(value); - } - - get nzIncluded(): boolean { - return this._included; - } + @Input() nzMarksArray: ExtendedMark[]; + @Input() nzMin: number; + @Input() nzMax: number; + @Input() @InputBoolean() nzVertical = false; + @Input() @InputBoolean() nzIncluded = false; - // TODO: using named interface - attrs: Array<{ id: number, value: number, offset: number, classes: { [ key: string ]: boolean }, style: object, label: Mark }>; // points for inner use + marks: DisplayedMark[]; ngOnChanges(changes: SimpleChanges): void { if (changes.nzMarksArray) { - this.buildAttrs(); + this.buildMarks(); } if (changes.nzMarksArray || changes.nzLowerBound || changes.nzUpperBound) { this.togglePointActive(); } } - trackById(index: number, attr: { id: number, value: number, offset: number, classes: { [ key: string ]: boolean }, style: object, label: Mark }): number { - return attr.id; + trackById(index: number, mark: DisplayedMark): number { + return mark.value; } - buildAttrs(): void { + private buildMarks(): void { const range = this.nzMax - this.nzMin; - this.attrs = this.nzMarksArray.map(mark => { + + this.marks = this.nzMarksArray.map(mark => { const { value, offset, config } = mark; - // calc styles - let label = config; - let style: object; - if (this.nzVertical) { - style = { - marginBottom: '-50%', - bottom : `${(value - this.nzMin) / range * 100}%` - }; - } else { - const marksCount = this.nzMarksArray.length; - const unit = 100 / (marksCount - 1); - const markWidth = unit * 0.9; - style = { - width : `${markWidth}%`, - marginLeft: `${-markWidth / 2}%`, - left : `${(value - this.nzMin) / range * 100}%` - }; - } - // custom configuration - if (typeof config === 'object') { - label = config.label; - if (config.style) { - style = { ...style, ...config.style }; - } - } + const style = this.buildStyles(value, range, config); + const label = isConfigAObject(config) ? config.label : config; + return { - id : value, - value, + label, offset, - classes: { - [ `${this.nzClassName}-text` ]: true - }, style, - label + value, + config, + active: false }; - }); // END - map + }); } - togglePointActive(): void { - if (this.attrs && this.nzLowerBound !== null && this.nzUpperBound !== null) { - this.attrs.forEach(attr => { - const value = attr.value; - const isActive = (!this.nzIncluded && value === this.nzUpperBound) || - (this.nzIncluded && value <= this.nzUpperBound && value >= this.nzLowerBound); - attr.classes[ `${this.nzClassName}-text-active` ] = isActive; - }); - } - } + private buildStyles(value: number, range: number, config: Mark): { [ key: string ]: string } { + let style; -} + if (this.nzVertical) { + style = { + marginBottom: '-50%', + bottom : `${(value - this.nzMin) / range * 100}%` + }; + } else { + const marksCount = this.nzMarksArray.length; + const unit = 100 / (marksCount - 1); + const markWidth = unit * 0.9; + style = { + width : `${markWidth}%`, + marginLeft: `${-markWidth / 2}%`, + left : `${(value - this.nzMin) / range * 100}%` + }; + } -// DEFINITIONS + if (isConfigAObject(config) && config.style) { + style = { ...style, ...config.style }; + } -export type Mark = string | { - style: object; - label: string; -}; + return style; + } -export class Marks { - number: Mark; -} + private togglePointActive(): void { + if (this.marks && this.nzLowerBound !== null && this.nzUpperBound !== null) { + this.marks.forEach(mark => { + const value = mark.value; + const isActive = + (!this.nzIncluded && value === this.nzUpperBound) || + (this.nzIncluded && value <= this.nzUpperBound && value >= this.nzLowerBound); -// TODO: extends Array could cause unexpected behavior when targeting es5 or below -export class MarksArray extends Array<{ value: number, offset: number, config: Mark }> { - [ index: number ]: { - value: number; - offset: number; - config: Mark; + mark.active = isActive; + }); + } } } diff --git a/components/slider/nz-slider-step.component.html b/components/slider/nz-slider-step.component.html index 4855c1784e1..ea884ac7f8e 100644 --- a/components/slider/nz-slider-step.component.html +++ b/components/slider/nz-slider-step.component.html @@ -1,3 +1,8 @@ -
- +
+ +
\ No newline at end of file diff --git a/components/slider/nz-slider-step.component.ts b/components/slider/nz-slider-step.component.ts index 0f8a9f3a943..1d0e1ce72df 100644 --- a/components/slider/nz-slider-step.component.ts +++ b/components/slider/nz-slider-step.component.ts @@ -1,89 +1,66 @@ -import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core'; -import { toBoolean } from '../core/util/convert'; +import { InputBoolean } from '../core/util/convert'; -import { MarksArray } from './nz-slider-marks.component'; +import { DisplayedStep, ExtendedMark } from './nz-slider-definitions'; @Component({ + changeDetection : ChangeDetectionStrategy.OnPush, + encapsulation : ViewEncapsulation.None, selector : 'nz-slider-step', preserveWhitespaces: false, templateUrl : './nz-slider-step.component.html' }) export class NzSliderStepComponent implements OnChanges { - private _vertical = false; - private _included = false; - - // Dynamic properties @Input() nzLowerBound: number = null; @Input() nzUpperBound: number = null; - @Input() nzMarksArray: MarksArray; - - // Static properties - @Input() nzPrefixCls: string; - - @Input() - set nzVertical(value: boolean) { // Required - this._vertical = toBoolean(value); - } - - get nzVertical(): boolean { - return this._vertical; - } - - @Input() - set nzIncluded(value: boolean) { - this._included = toBoolean(value); - } - - get nzIncluded(): boolean { - return this._included; - } + @Input() nzMarksArray: ExtendedMark[]; + @Input() @InputBoolean() nzVertical = false; + @Input() @InputBoolean() nzIncluded = false; - // TODO: using named interface - attrs: Array<{ id: number, value: number, offset: number, classes: { [ key: string ]: boolean }, style: object }>; + steps: DisplayedStep[]; ngOnChanges(changes: SimpleChanges): void { if (changes.nzMarksArray) { - this.buildAttrs(); + this.buildSteps(); } if (changes.nzMarksArray || changes.nzLowerBound || changes.nzUpperBound) { this.togglePointActive(); } } - trackById(index: number, attr: { id: number, value: number, offset: number, classes: { [ key: string ]: boolean }, style: object }): number { - return attr.id; + trackById(index: number, step: DisplayedStep): number { + return step.value; } - buildAttrs(): void { + private buildSteps(): void { const orient = this.nzVertical ? 'bottom' : 'left'; - const prefixCls = this.nzPrefixCls; - this.attrs = this.nzMarksArray.map(mark => { - const { value, offset } = mark; + + this.steps = this.nzMarksArray.map(mark => { + const { value, offset, config } = mark; + return { - id : value, value, offset, - style : { + config, + active: false, + style : { [ orient ]: `${offset}%` - }, - classes: { - [ `${prefixCls}-dot` ] : true, - [ `${prefixCls}-dot-active` ]: false } }; }); } - togglePointActive(): void { - if (this.attrs && this.nzLowerBound !== null && this.nzUpperBound !== null) { - this.attrs.forEach(attr => { - const value = attr.value; - const isActive = (!this.nzIncluded && value === this.nzUpperBound) || + private togglePointActive(): void { + if (this.steps && this.nzLowerBound !== null && this.nzUpperBound !== null) { + this.steps.forEach(step => { + const value = step.value; + const isActive = + (!this.nzIncluded && value === this.nzUpperBound) || (this.nzIncluded && value <= this.nzUpperBound && value >= this.nzLowerBound); - attr.classes[ `${this.nzPrefixCls}-dot-active` ] = isActive; + + step.active = isActive; }); } } - } diff --git a/components/slider/nz-slider-track.component.html b/components/slider/nz-slider-track.component.html index ecd5161a7f8..9c01d454036 100644 --- a/components/slider/nz-slider-track.component.html +++ b/components/slider/nz-slider-track.component.html @@ -1 +1 @@ -
\ No newline at end of file +
\ No newline at end of file diff --git a/components/slider/nz-slider-track.component.ts b/components/slider/nz-slider-track.component.ts index d1911ed116d..f089c26c51f 100644 --- a/components/slider/nz-slider-track.component.ts +++ b/components/slider/nz-slider-track.component.ts @@ -1,42 +1,29 @@ -import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core'; -import { toBoolean } from '../core/util/convert'; +import { InputBoolean } from '../core/util/convert'; + +export interface NzSliderTrackStyle { + bottom?: string; + height?: string; + left?: string; + width?: string; + visibility?: string; +} @Component({ + changeDetection : ChangeDetectionStrategy.OnPush, + encapsulation : ViewEncapsulation.None, selector : 'nz-slider-track', preserveWhitespaces: false, templateUrl : './nz-slider-track.component.html' }) export class NzSliderTrackComponent implements OnChanges { - private _vertical = false; - private _included = false; - - // Dynamic properties @Input() nzOffset; @Input() nzLength; + @Input() @InputBoolean() nzVertical = false; + @Input() @InputBoolean() nzIncluded = false; - // Static properties - @Input() nzClassName; - - @Input() - set nzVertical(value: boolean) { // Required - this._vertical = toBoolean(value); - } - - get nzVertical(): boolean { - return this._vertical; - } - - @Input() - set nzIncluded(value: boolean) { - this._included = toBoolean(value); - } - - get nzIncluded(): boolean { - return this._included; - } - - style: { bottom?: string, height?: string, left?: string, width?: string, visibility?: string } = {}; + style: NzSliderTrackStyle = {}; ngOnChanges(changes: SimpleChanges): void { if (changes.nzIncluded) { @@ -46,11 +33,14 @@ export class NzSliderTrackComponent implements OnChanges { if (this.nzVertical) { this.style.bottom = `${this.nzOffset}%`; this.style.height = `${this.nzLength}%`; + this.style.left = null; + this.style.width = null; } else { this.style.left = `${this.nzOffset}%`; this.style.width = `${this.nzLength}%`; + this.style.bottom = null; + this.style.height = null; } } } - } diff --git a/components/slider/nz-slider.component.html b/components/slider/nz-slider.component.html index ff1f1592fd6..6d9a168471d 100644 --- a/components/slider/nz-slider.component.html +++ b/components/slider/nz-slider.component.html @@ -1,37 +1,36 @@ -
+
- + + [nzIncluded]="nzIncluded"> - + + [nzIncluded]="nzIncluded">
\ No newline at end of file diff --git a/components/slider/nz-slider.component.ts b/components/slider/nz-slider.component.ts index f55eb2fdc0b..99bbb5e29af 100644 --- a/components/slider/nz-slider.component.ts +++ b/components/slider/nz-slider.component.ts @@ -1,6 +1,7 @@ -/* tslint:disable:variable-name */ import { forwardRef, + ChangeDetectionStrategy, + ChangeDetectorRef, Component, ElementRef, EventEmitter, @@ -10,39 +11,25 @@ import { OnInit, Output, SimpleChanges, - ViewChild + ViewChild, + ViewEncapsulation } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { fromEvent, merge, Observable, Subscription } from 'rxjs'; import { distinctUntilChanged, filter, map, pluck, takeUntil, tap } from 'rxjs/operators'; -import { toBoolean } from '../core/util/convert'; +import { InputBoolean } from '../core/util/convert'; +import { getElementOffset, silentEvent, MouseTouchObserverConfig } from '../core/util/dom'; -import { Marks, MarksArray } from './nz-slider-marks.component'; -import { NzSliderService } from './nz-slider.service'; +import { arraysEqual, shallowCopyArray } from '../core/util/array'; +import { ensureNumberInRange, getPercent, getPrecision } from '../core/util/number'; -export type SliderValue = number[] | number; - -export class SliderHandle { - offset: number; - value: number; - active: boolean; -} - -interface MouseTouchObserverConfig { - start: string; - move: string; - end: string; - pluckKey: string[]; - - filter?(e: Event): boolean; - - startPlucked$?: Observable; - end$?: Observable; - moveResolved$?: Observable; -} +import { isValueARange, Marks, SliderHandler, SliderShowTooltip, SliderValue } from './nz-slider-definitions'; +import { getValueTypeNotMatchError } from './nz-slider-error'; @Component({ + changeDetection : ChangeDetectionStrategy.OnPush, + encapsulation : ViewEncapsulation.None, selector : 'nz-slider', preserveWhitespaces: false, providers : [ { @@ -53,150 +40,82 @@ interface MouseTouchObserverConfig { templateUrl : './nz-slider.component.html' }) export class NzSliderComponent implements ControlValueAccessor, OnInit, OnChanges, OnDestroy { + @ViewChild('slider') slider: ElementRef; - // Debugging - @Input() nzDebugId: number | string = null; // set this id will print debug informations to console - - // Dynamic property settings - @Input() - set nzDisabled(value: boolean) { - this._disabled = toBoolean(value); - } - - get nzDisabled(): boolean { - return this._disabled; - } - - // Static configurations (properties that can only specify once) - @Input() nzStep = 1; + @Input() @InputBoolean() nzDisabled = false; + @Input() @InputBoolean() nzDots: boolean = false; + @Input() @InputBoolean() nzIncluded: boolean = true; + @Input() @InputBoolean() nzRange: boolean = false; + @Input() @InputBoolean() nzVertical: boolean = false; + @Input() nzDefaultValue: SliderValue = null; @Input() nzMarks: Marks = null; - @Input() nzMin = 0; @Input() nzMax = 100; - @Input() nzDefaultValue: SliderValue = null; + @Input() nzMin = 0; + @Input() nzStep = 1; + @Input() nzTooltipVisible: SliderShowTooltip = 'default'; @Input() nzTipFormatter: (value: number) => string; - @Output() readonly nzOnAfterChange = new EventEmitter(); - - @Input() - set nzVertical(value: boolean) { - this._vertical = toBoolean(value); - } - - get nzVertical(): boolean { - return this._vertical; - } - - @Input() - set nzRange(value: boolean) { - this._range = toBoolean(value); - } - - get nzRange(): boolean { - return this._range; - } - - @Input() - set nzDots(value: boolean) { - this._dots = toBoolean(value); - } - - get nzDots(): boolean { - return this._dots; - } - - @Input() - set nzIncluded(value: boolean) { - this._included = toBoolean(value); - } - - get nzIncluded(): boolean { - return this._included; - } - // Inside properties - private _disabled = false; - private _dots = false; - private _included = true; - private _range = false; - private _vertical = false; + @Output() readonly nzOnAfterChange = new EventEmitter(); value: SliderValue = null; // CORE value state - @ViewChild('slider') slider: ElementRef; sliderDOM: HTMLDivElement; cacheSliderStart: number = null; cacheSliderLength: number = null; - prefixCls = 'ant-slider'; - classMap: object; activeValueIndex: number = null; // Current activated handle's index ONLY for range=true track = { offset: null, length: null }; // Track's offset and length - handles: SliderHandle[]; // Handles' offset - marksArray: Marks[]; // "marks" in array type with more data & FILTER out the invalid mark + handles: SliderHandler[]; // Handles' offset + marksArray: Marks[]; // "steps" in array type with more data & FILTER out the invalid mark bounds = { lower: null, upper: null }; // now for nz-slider-step - onValueChange: (value: SliderValue) => void; // Used by ngModel. BUG: onValueChange() will not success to effect the "value" variable ( [(ngModel)]="value" ) when the first initializing, except using "nextTick" functionality (MAY angular2's problem ?) - onTouched: () => void = () => { - } // onTouch function registered via registerOnTouch (ControlValueAccessor). isDragging = false; // Current dragging state - // Events observables & subscriptions - dragstart$: Observable; - dragmove$: Observable; - dragend$: Observable; - dragstart_: Subscription; - dragmove_: Subscription; - dragend_: Subscription; - - // |-------------------------------------------------------------------------------------------- - // | value accessors & ngModel accessors - // |-------------------------------------------------------------------------------------------- - - setValue(val: SliderValue, isWriteValue: boolean = false): void { - if (isWriteValue) { // [ngModel-writeValue]: Formatting before setting value, always update current value, but trigger onValueChange ONLY when the "formatted value" not equals "input value" - this.value = this.formatValue(val); - this.log(`[ngModel:setValue/writeValue]Update track & handles`); - this.updateTrackAndHandles(); - // if (!this.isValueEqual(this.value, val)) { - // this.log(`[ngModel:setValue/writeValue]onValueChange`, val); - // if (this.onValueChange) { // NOTE: onValueChange will be unavailable when writeValue() called at the first time - // this.onValueChange(this.value); - // } - // } - } else { // [Normal]: setting value, ONLY check changed, then update and trigger onValueChange - if (!this.isValueEqual(this.value, val)) { - this.value = val; - this.log(`[Normal:setValue]Update track & handles`); - this.updateTrackAndHandles(); - this.log(`[Normal:setValue]onValueChange`, val); - if (this.onValueChange) { // NOTE: onValueChange will be unavailable when writeValue() called at the first time - this.onValueChange(this.value); - } - } + private dragStart$: Observable; + private dragMove$: Observable; + private dragEnd$: Observable; + private dragStart_: Subscription; + private dragMove_: Subscription; + private dragEnd_: Subscription; + + constructor(private cdr: ChangeDetectorRef) {} + + ngOnInit(): void { + this.assertValueTypeMatch(this.nzDefaultValue); + this.handles = this.generateHandles(this.nzRange ? 2 : 1); + this.sliderDOM = this.slider.nativeElement; + this.marksArray = this.nzMarks ? this.generateMarkItems(this.nzMarks) : null; + this.createDraggingObservables(); + this.toggleDragDisabled(this.nzDisabled); + + if (this.getValue() === null) { + this.setValue(this.formatValue(null)); } } - getValue(cloneAndSort: boolean = false): SliderValue { - // TODO: using type guard, remove type cast - if (cloneAndSort && this.nzRange) { // clone & sort range values - return this.utils.cloneArray(this.value as number[]).sort((a, b) => a - b); + ngOnChanges(changes: SimpleChanges): void { + const { nzDisabled, nzMarks, nzRange } = changes; + + if (nzDisabled && !nzDisabled.firstChange) { + this.toggleDragDisabled(nzDisabled.currentValue); + } else if (nzMarks && !nzMarks.firstChange) { + this.marksArray = this.nzMarks ? this.generateMarkItems(this.nzMarks) : null; + } else if (nzRange && !nzRange.firstChange) { + this.setValue(this.formatValue(null)); } - return this.value; } - // clone & sort current value and convert them to offsets, then return the new one - getValueToOffset(value?: SliderValue): SliderValue { - let normalizedValue = value; - if (typeof normalizedValue === 'undefined') { - normalizedValue = this.getValue(true); - } - // TODO: using type guard, remove type cast - return this.nzRange ? - (normalizedValue as number[]).map(val => this.valueToOffset(val)) : - this.valueToOffset(normalizedValue as number); + ngOnDestroy(): void { + this.unsubscribeDrag(); } writeValue(val: SliderValue): void { - this.log(`[ngModel/writeValue]current writing value = `, val); this.setValue(val, true); } + onValueChange(value: SliderValue): void { + } + + onTouched(): void { + } + registerOnChange(fn: (value: SliderValue) => void): void { this.onValueChange = fn; } @@ -208,73 +127,51 @@ export class NzSliderComponent implements ControlValueAccessor, OnInit, OnChange setDisabledState(isDisabled: boolean): void { this.nzDisabled = isDisabled; this.toggleDragDisabled(isDisabled); - this.setClassMap(); - } - - // |-------------------------------------------------------------------------------------------- - // | Lifecycle hooks - // |-------------------------------------------------------------------------------------------- - - constructor(private utils: NzSliderService) { } - // initialize event binding, class init, etc. (called only once) - ngOnInit(): void { - // initial checking - this.checkValidValue(this.nzDefaultValue); // check nzDefaultValue - // default handles - this.handles = this._generateHandles(this.nzRange ? 2 : 1); - // initialize - this.sliderDOM = this.slider.nativeElement; - if (this.getValue() === null) { - this.setValue(this.formatValue(null)); - } // init with default value - this.marksArray = this.nzMarks === null ? null : this.toMarksArray(this.nzMarks); - // event bindings - this.createDrag(); - // initialize drag's disabled status - this.toggleDragDisabled(this.nzDisabled); - // the first time to init classes - this.setClassMap(); + private setValue(value: SliderValue, isWriteValue: boolean = false): void { + if (isWriteValue) { + this.value = this.formatValue(value); + this.updateTrackAndHandles(); + } else if (!this.valuesEqual(this.value, value)) { + this.value = value; + this.updateTrackAndHandles(); + this.onValueChange(this.getValue(true)); + } } - ngOnChanges(changes: SimpleChanges): void { - const { nzDisabled, nzMarks, nzRange } = changes; - if (nzDisabled && !nzDisabled.firstChange) { - this.toggleDragDisabled(nzDisabled.currentValue); - this.setClassMap(); - } else if (nzMarks && !nzMarks.firstChange) { - this.marksArray = this.nzMarks ? this.toMarksArray(this.nzMarks) : null; - } else if (nzRange && !nzRange.firstChange) { - this.setValue(this.formatValue(null)); // Change to default value when nzRange changed + private getValue(cloneAndSort: boolean = false): SliderValue { + if (cloneAndSort && isValueARange(this.value)) { + return shallowCopyArray(this.value).sort((a, b) => a - b); } + return this.value; } - ngOnDestroy(): void { - this.unsubscribeDrag(); - } + /** + * Clone & sort current value and convert them to offsets, then return the new one. + */ + private getValueToOffset(value?: SliderValue): SliderValue { + let normalizedValue = value; - // |-------------------------------------------------------------------------------------------- - // | Basic flow functions - // |-------------------------------------------------------------------------------------------- + if (typeof normalizedValue === 'undefined') { + normalizedValue = this.getValue(true); + } - setClassMap(): void { - this.classMap = { - [ this.prefixCls ] : true, - [ `${this.prefixCls}-disabled` ] : this.nzDisabled, - [ `${this.prefixCls}-vertical` ] : this.nzVertical, - [ `${this.prefixCls}-with-marks` ]: this.marksArray ? this.marksArray.length : 0 - }; + return isValueARange(normalizedValue) + ? normalizedValue.map(val => this.valueToOffset(val)) + : this.valueToOffset(normalizedValue); } - // find the cloest value to be activated (only for range = true) - setActiveValueIndex(pointerValue: number): void { - if (this.nzRange) { + /** + * Find the closest value to be activated (only for range = true). + */ + private setActiveValueIndex(pointerValue: number): void { + const value = this.getValue(); + if (isValueARange(value)) { let minimal = null; let gap; let activeIndex; - // TODO: using type guard, remove type cast - (this.getValue() as number[]).forEach((val, index) => { + value.forEach((val, index) => { gap = Math.abs(pointerValue - val); if (minimal === null || gap < minimal) { minimal = gap; @@ -285,10 +182,9 @@ export class NzSliderComponent implements ControlValueAccessor, OnInit, OnChange } } - setActiveValue(pointerValue: number): void { - if (this.nzRange) { - // TODO: using type guard, remove type cast - const newValue = this.utils.cloneArray(this.value as number[]); + private setActiveValue(pointerValue: number): void { + if (isValueARange(this.value)) { + const newValue = shallowCopyArray(this.value); newValue[ this.activeValueIndex ] = pointerValue; this.setValue(newValue); } else { @@ -296,7 +192,10 @@ export class NzSliderComponent implements ControlValueAccessor, OnInit, OnChange } } - updateTrackAndHandles(): void { + /** + * Update track and handles' position and length. + */ + private updateTrackAndHandles(): void { const value = this.getValue(); const offset = this.getValueToOffset(value); const valueSorted = this.getValue(true); @@ -310,131 +209,109 @@ export class NzSliderComponent implements ControlValueAccessor, OnInit, OnChange }); [ this.bounds.lower, this.bounds.upper ] = boundParts; [ this.track.offset, this.track.length ] = trackParts; - } - toMarksArray(marks: Marks): Marks[] { - const marksArray = []; - for (const key in marks) { - const mark = marks[ key ]; - const val = typeof key === 'number' ? key : parseFloat(key); - if (val < this.nzMin || val > this.nzMax) { - continue; - } - marksArray.push({ value: val, offset: this.valueToOffset(val), config: mark }); - } - return marksArray; + this.cdr.markForCheck(); } - // |-------------------------------------------------------------------------------------------- - // | Event listeners & bindings - // |-------------------------------------------------------------------------------------------- - - onDragStart(value: number): void { - this.log('[onDragStart]dragging value = ', value); + private onDragStart(value: number): void { this.toggleDragMoving(true); - // cache DOM layout/reflow operations this.cacheSliderProperty(); - // trigger drag start this.setActiveValueIndex(value); this.setActiveValue(value); - // Tooltip visibility of handles - this._showHandleTooltip(this.nzRange ? this.activeValueIndex : 0); + this.showHandleTooltip(this.nzRange ? this.activeValueIndex : 0); } - onDragMove(value: number): void { - this.log('[onDragMove]dragging value = ', value); - // trigger drag moving + private onDragMove(value: number): void { this.setActiveValue(value); + this.cdr.markForCheck(); } - onDragEnd(): void { - this.log('[onDragEnd]'); - this.toggleDragMoving(false); + private onDragEnd(): void { this.nzOnAfterChange.emit(this.getValue(true)); - // remove cache DOM layout/reflow operations + this.toggleDragMoving(false); this.cacheSliderProperty(true); - // Hide all tooltip - this._hideAllHandleTooltip(); + this.hideAllHandleTooltip(); + this.cdr.markForCheck(); } - createDrag(): void { + /** + * Create user interactions handles. + */ + private createDraggingObservables(): void { const sliderDOM = this.sliderDOM; const orientField = this.nzVertical ? 'pageY' : 'pageX'; const mouse: MouseTouchObserverConfig = { - start : 'mousedown', move: 'mousemove', end: 'mouseup', + start : 'mousedown', + move : 'mousemove', + end : 'mouseup', pluckKey: [ orientField ] }; const touch: MouseTouchObserverConfig = { - start : 'touchstart', move: 'touchmove', end: 'touchend', + start : 'touchstart', + move : 'touchmove', + end : 'touchend', pluckKey: [ 'touches', '0', orientField ], - filter : (e: MouseEvent | TouchEvent) => !this.utils.isNotTouchEvent(e as TouchEvent) + filter : (e: MouseEvent | TouchEvent) => e instanceof TouchEvent }; - // make observables + [ mouse, touch ].forEach(source => { const { start, move, end, pluckKey, filter: filterFunc = (() => true) } = source; - // start + source.startPlucked$ = fromEvent(sliderDOM, start).pipe( filter(filterFunc), - tap(this.utils.pauseEvent), + tap(silentEvent), pluck(...pluckKey), map((position: number) => this.findClosestValue(position)) ); - // end source.end$ = fromEvent(document, end); - // resolve move source.moveResolved$ = fromEvent(document, move).pipe( filter(filterFunc), - tap(this.utils.pauseEvent), + tap(silentEvent), pluck(...pluckKey), distinctUntilChanged(), map((position: number) => this.findClosestValue(position)), distinctUntilChanged(), takeUntil(source.end$) ); - // merge to become moving - // source.move$ = source.startPlucked$.mergeMapTo(source.moveResolved$); }); - // merge mouse and touch observables - this.dragstart$ = merge(mouse.startPlucked$, touch.startPlucked$); - // this.dragmove$ = Observable.merge(mouse.move$, touch.move$); - this.dragmove$ = merge(mouse.moveResolved$, touch.moveResolved$); - this.dragend$ = merge(mouse.end$, touch.end$); + + this.dragStart$ = merge(mouse.startPlucked$, touch.startPlucked$); + this.dragMove$ = merge(mouse.moveResolved$, touch.moveResolved$); + this.dragEnd$ = merge(mouse.end$, touch.end$); } - subscribeDrag(periods: string[] = [ 'start', 'move', 'end' ]): void { - this.log('[subscribeDrag]this.dragstart$ = ', this.dragstart$); - if (periods.indexOf('start') !== -1 && this.dragstart$ && !this.dragstart_) { - this.dragstart_ = this.dragstart$.subscribe(this.onDragStart.bind(this)); + private subscribeDrag(periods: string[] = [ 'start', 'move', 'end' ]): void { + if (periods.indexOf('start') !== -1 && this.dragStart$ && !this.dragStart_) { + this.dragStart_ = this.dragStart$.subscribe(this.onDragStart.bind(this)); } - if (periods.indexOf('move') !== -1 && this.dragmove$ && !this.dragmove_) { - this.dragmove_ = this.dragmove$.subscribe(this.onDragMove.bind(this)); + if (periods.indexOf('move') !== -1 && this.dragMove$ && !this.dragMove_) { + this.dragMove_ = this.dragMove$.subscribe(this.onDragMove.bind(this)); } - if (periods.indexOf('end') !== -1 && this.dragend$ && !this.dragend_) { - this.dragend_ = this.dragend$.subscribe(this.onDragEnd.bind(this)); + if (periods.indexOf('end') !== -1 && this.dragEnd$ && !this.dragEnd_) { + this.dragEnd_ = this.dragEnd$.subscribe(this.onDragEnd.bind(this)); } } - unsubscribeDrag(periods: string[] = [ 'start', 'move', 'end' ]): void { - this.log('[unsubscribeDrag]this.dragstart_ = ', this.dragstart_); - if (periods.indexOf('start') !== -1 && this.dragstart_) { - this.dragstart_.unsubscribe(); - this.dragstart_ = null; + private unsubscribeDrag(periods: string[] = [ 'start', 'move', 'end' ]): void { + if (periods.indexOf('start') !== -1 && this.dragStart_) { + this.dragStart_.unsubscribe(); + this.dragStart_ = null; } - if (periods.indexOf('move') !== -1 && this.dragmove_) { - this.dragmove_.unsubscribe(); - this.dragmove_ = null; + if (periods.indexOf('move') !== -1 && this.dragMove_) { + this.dragMove_.unsubscribe(); + this.dragMove_ = null; } - if (periods.indexOf('end') !== -1 && this.dragend_) { - this.dragend_.unsubscribe(); - this.dragend_ = null; + if (periods.indexOf('end') !== -1 && this.dragEnd_) { + this.dragEnd_.unsubscribe(); + this.dragEnd_ = null; } } - toggleDragMoving(movable: boolean): void { + private toggleDragMoving(movable: boolean): void { const periods = [ 'move', 'end' ]; if (movable) { this.isDragging = true; @@ -445,7 +322,7 @@ export class NzSliderComponent implements ControlValueAccessor, OnInit, OnChange } } - toggleDragDisabled(disabled: boolean): void { + private toggleDragDisabled(disabled: boolean): void { if (disabled) { this.unsubscribeDrag(); } else { @@ -453,137 +330,119 @@ export class NzSliderComponent implements ControlValueAccessor, OnInit, OnChange } } - // |-------------------------------------------------------------------------------------------- - // | Util functions (tools) - // |-------------------------------------------------------------------------------------------- - - // find the closest value depend on pointer's position - findClosestValue(position: number): number { + private findClosestValue(position: number): number { const sliderStart = this.getSliderStartPosition(); const sliderLength = this.getSliderLength(); - const ratio = this.utils.correctNumLimit((position - sliderStart) / sliderLength, 0, 1); + const ratio = ensureNumberInRange((position - sliderStart) / sliderLength, 0, 1); const val = (this.nzMax - this.nzMin) * (this.nzVertical ? 1 - ratio : ratio) + this.nzMin; const points = (this.nzMarks === null ? [] : Object.keys(this.nzMarks).map(parseFloat)); - // push closest step if (this.nzStep !== null && !this.nzDots) { const closestOne = Math.round(val / this.nzStep) * this.nzStep; points.push(closestOne); } - // calculate gaps const gaps = points.map(point => Math.abs(val - point)); const closest = points[ gaps.indexOf(Math.min(...gaps)) ]; - // return the fixed - return this.nzStep === null ? closest : - parseFloat(closest.toFixed(this.utils.getPrecision(this.nzStep))); + return this.nzStep === null ? closest : parseFloat(closest.toFixed(getPrecision(this.nzStep))); } - valueToOffset(value: number): number { - return this.utils.valueToOffset(this.nzMin, this.nzMax, value); + private valueToOffset(value: number): number { + return getPercent(this.nzMin, this.nzMax, value); } - getSliderStartPosition(): number { + private getSliderStartPosition(): number { if (this.cacheSliderStart !== null) { return this.cacheSliderStart; } - const offset = this.utils.getElementOffset(this.sliderDOM); + const offset = getElementOffset(this.sliderDOM); return this.nzVertical ? offset.top : offset.left; } - getSliderLength(): number { + private getSliderLength(): number { if (this.cacheSliderLength !== null) { return this.cacheSliderLength; } const sliderDOM = this.sliderDOM; - return this.nzVertical ? - sliderDOM.clientHeight : sliderDOM.clientWidth; + return this.nzVertical ? sliderDOM.clientHeight : sliderDOM.clientWidth; } - // cache DOM layout/reflow operations for performance (may not necessary?) - cacheSliderProperty(remove: boolean = false): void { + /** + * Cache DOM layout/reflow operations for performance (may not necessary?) + */ + private cacheSliderProperty(remove: boolean = false): void { this.cacheSliderStart = remove ? null : this.getSliderStartPosition(); this.cacheSliderLength = remove ? null : this.getSliderLength(); } - formatValue(value: SliderValue): SliderValue { // NOTE: will return new value + private formatValue(value: SliderValue): SliderValue { let res = value; - if (!this.checkValidValue(value)) { // if empty, use default value - res = this.nzDefaultValue === null ? - (this.nzRange ? [ this.nzMin, this.nzMax ] : this.nzMin) : this.nzDefaultValue; - } else { // format - // TODO: using type guard, remove type cast - res = this.nzRange ? - (value as number[]).map(val => this.utils.correctNumLimit(val, this.nzMin, this.nzMax)) : - this.utils.correctNumLimit(value as number, this.nzMin, this.nzMax); + if (!this.assertValueValid(value)) { + res = this.nzDefaultValue === null + ? (this.nzRange ? [ this.nzMin, this.nzMax ] : this.nzMin) + : this.nzDefaultValue; + } else { + res = isValueARange(value) + ? value.map(val => ensureNumberInRange(val, this.nzMin, this.nzMax)) + : ensureNumberInRange(value, this.nzMin, this.nzMax); } return res; } - // check if value is valid and throw error if value-type/range not match - checkValidValue(value: SliderValue): boolean { - const range = this.nzRange; + /** + * Check if value is valid and throw error if value-type/range not match. + */ + private assertValueValid(value: SliderValue): boolean { if (value === null || value === undefined) { return false; - } // it's an invalid value, just return - const isArray = Array.isArray(value); - if (!Array.isArray(value)) { - let parsedValue: number = value; - if (typeof value !== 'number') { - parsedValue = parseFloat(value); - } - if (isNaN(parsedValue)) { - return false; - } // it's an invalid value, just return } - if (isArray !== !!range) { // value type not match - throw new Error(`The "nzRange" can't match the "nzValue"'s type, please check these properties: "nzRange", "nzValue", "nzDefaultValue".`); + if (!Array.isArray(value) && isNaN(typeof value !== 'number' ? parseFloat(value) : value)) { + return false; } - return true; + return this.assertValueTypeMatch(value); } - isValueEqual(value: SliderValue, val: SliderValue): boolean { - if (typeof value !== typeof val) { - return false; - } - if (Array.isArray(value)) { - const len = value.length; - for (let i = 0; i < len; i++) { - if (value[ i ] !== val[ i ]) { - return false; - } - } - return true; - } else { - return value === val; + /** + * Assert that if `this.nzRange` is `true`, value is also a range, vice versa. + */ + private assertValueTypeMatch(value: SliderValue): boolean { + if (isValueARange(value) !== this.nzRange) { + throw getValueTypeNotMatchError(); } + return true; } - // print debug info - // TODO: should not kept in component - /* tslint:disable-next-line:no-any */ - log(...messages: any[]): void { - if (this.nzDebugId !== null) { - const args = [ `[nz-slider][#${this.nzDebugId}] ` ].concat(Array.prototype.slice.call(arguments)); - console.log.apply(null, args); + private valuesEqual(valA: SliderValue, valB: SliderValue): boolean { + if (typeof valA !== typeof valB) { + return false; } + return isValueARange(valA) && isValueARange(valB) ? arraysEqual(valA, valB) : valA === valB; } - // Show one handle's tooltip and hide others' - private _showHandleTooltip(handleIndex: number = 0): void { + /** + * Show one handle's tooltip and hide others'. + */ + private showHandleTooltip(handleIndex: number = 0): void { this.handles.forEach((handle, index) => { - this.handles[ index ].active = index === handleIndex; + handle.active = index === handleIndex; }); } - private _hideAllHandleTooltip(): void { + private hideAllHandleTooltip(): void { this.handles.forEach(handle => handle.active = false); } - private _generateHandles(amount: number): SliderHandle[] { - const handles: SliderHandle[] = []; - for (let i = 0; i < amount; i++) { - handles.push({ offset: null, value: null, active: false }); - } - return handles; + private generateHandles(amount: number): SliderHandler[] { + return Array(amount).fill(0).map(() => ({ offset: null, value: null, active: false })); } + private generateMarkItems(marks: Marks): Marks[] | null { + const marksArray = []; + for (const key in marks) { + const mark = marks[ key ]; + const val = typeof key === 'number' ? key : parseFloat(key); + if (val >= this.nzMin && val <= this.nzMax) { + marksArray.push({ value: val, offset: this.valueToOffset(val), config: mark }); + } + } + return marksArray.length ? marksArray : null; + } } diff --git a/components/slider/nz-slider.module.ts b/components/slider/nz-slider.module.ts index 711c7b71d05..d95f06ab833 100644 --- a/components/slider/nz-slider.module.ts +++ b/components/slider/nz-slider.module.ts @@ -8,12 +8,22 @@ import { NzSliderMarksComponent } from './nz-slider-marks.component'; import { NzSliderStepComponent } from './nz-slider-step.component'; import { NzSliderTrackComponent } from './nz-slider-track.component'; import { NzSliderComponent } from './nz-slider.component'; -import { NzSliderService } from './nz-slider.service'; @NgModule({ - exports: [ NzSliderComponent, NzSliderTrackComponent, NzSliderHandleComponent, NzSliderStepComponent, NzSliderMarksComponent ], - declarations: [ NzSliderComponent, NzSliderTrackComponent, NzSliderHandleComponent, NzSliderStepComponent, NzSliderMarksComponent ], - imports: [ CommonModule, NzToolTipModule ], - providers: [ NzSliderService ] + exports: [ + NzSliderComponent, + NzSliderTrackComponent, + NzSliderHandleComponent, + NzSliderStepComponent, + NzSliderMarksComponent + ], + declarations: [ + NzSliderComponent, + NzSliderTrackComponent, + NzSliderHandleComponent, + NzSliderStepComponent, + NzSliderMarksComponent + ], + imports: [ CommonModule, NzToolTipModule ] }) export class NzSliderModule { } diff --git a/components/slider/nz-slider.service.ts b/components/slider/nz-slider.service.ts deleted file mode 100644 index e62466a4f24..00000000000 --- a/components/slider/nz-slider.service.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable() -export class NzSliderService { - - pauseEvent(e: Event): void { - e.stopPropagation(); - e.preventDefault(); - } - - getPrecision(num: number): number { - const numStr = num.toString(); - const dotIndex = numStr.indexOf('.'); - return dotIndex >= 0 ? numStr.length - dotIndex - 1 : 0; - } - - cloneArray(arr: T[]): T[] { - return arr.slice(); - } - - isNotTouchEvent(e: TouchEvent): boolean { - return !e.touches || e.touches.length > 1 || - (e.type.toLowerCase() === 'touchend' && e.touches.length > 0); - } - - // convert value to offset in percent - valueToOffset(min: number, max: number, value: number): number { - return (value - min) / (max - min) * 100; - } - - correctNumLimit(num: number, min: number, max: number): number { - let res = +num; - if (isNaN(res)) { return min; } - if (num < min) { res = min; } else if (num > max) { res = max; } - return res; - } - - /** - * get the offset of an element relative to the document (Reference from jquery's offset()) - * @param elem HTMLElement ref - */ - getElementOffset(elem: HTMLElement): { top: number, left: number } { - // Return zeros for disconnected and hidden (display: none) elements (gh-2310) - // Support: IE <=11 only - // Running getBoundingClientRect on a - // disconnected node in IE throws an error - if (!elem.getClientRects().length) { - return { top: 0, left: 0 }; - } - // Get document-relative position by adding viewport scroll to viewport-relative gBCR - const rect = elem.getBoundingClientRect(); - const win = elem.ownerDocument.defaultView; - return { - top: rect.top + win.pageYOffset, - left: rect.left + win.pageXOffset - }; - } - -} diff --git a/components/slider/nz-slider.spec.ts b/components/slider/nz-slider.spec.ts index 7cc399e2ce3..775aaa5226c 100644 --- a/components/slider/nz-slider.spec.ts +++ b/components/slider/nz-slider.spec.ts @@ -1,18 +1,19 @@ import { OverlayContainer } from '@angular/cdk/overlay'; -import { Component, DebugElement, OnInit, ViewEncapsulation } from '@angular/core'; -import { fakeAsync, flush, inject, tick, ComponentFixture, TestBed } from '@angular/core/testing'; +import { Component, DebugElement, OnInit } from '@angular/core'; +import { fakeAsync, inject, tick, ComponentFixture, TestBed } from '@angular/core/testing'; import { AbstractControl, FormsModule, FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { dispatchMouseEvent } from '../core/testing'; +import { SliderShowTooltip } from './nz-slider-definitions'; import { NzSliderComponent } from './nz-slider.component'; import { NzSliderModule } from './nz-slider.module'; describe('NzSlider', () => { beforeEach(fakeAsync(() => { TestBed.configureTestingModule({ - imports: [ NzSliderModule, FormsModule, ReactiveFormsModule, NoopAnimationsModule ], + imports : [ NzSliderModule, FormsModule, ReactiveFormsModule, NoopAnimationsModule ], declarations: [ StandardSliderComponent, DisableSliderComponent, @@ -23,7 +24,8 @@ describe('NzSlider', () => { SliderWithValueGreaterThanMaxComponent, VerticalSliderComponent, MixedSliderComponent, - SliderWithFormControlComponent + SliderWithFormControlComponent, + SliderShowTooltipComponent ] }); @@ -123,9 +125,69 @@ describe('NzSlider', () => { expect(onChangeSpy).toHaveBeenCalledTimes(1); }); - it('should pass un-covered code testing', () => { - expect(sliderInstance.getValueToOffset()).toBe(sliderInstance.value); + }); + + describe('show tooltip', () => { + let fixture: ComponentFixture; + let sliderDebugElement: DebugElement; + let sliderNativeElement: HTMLElement; + let trackFillElement: HTMLElement; + let sliderInstance: NzSliderComponent; + let testComponent: SliderShowTooltipComponent; + let overlayContainer: OverlayContainer; + let overlayContainerElement: HTMLElement; + + beforeEach(inject([ OverlayContainer ], (oc: OverlayContainer) => { + overlayContainer = oc; + overlayContainerElement = oc.getContainerElement(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SliderShowTooltipComponent); + fixture.detectChanges(); + + testComponent = fixture.debugElement.componentInstance; + sliderDebugElement = fixture.debugElement.query(By.directive(NzSliderComponent)); + sliderInstance = sliderDebugElement.injector.get(NzSliderComponent); + sliderNativeElement = sliderInstance.sliderDOM; + trackFillElement = sliderNativeElement.querySelector('.ant-slider-track') as HTMLElement; }); + + it('should always display tooltips if set to `always`', fakeAsync(() => { + const handlerHost = sliderNativeElement.querySelector('nz-slider-handle'); + + testComponent.show = 'always'; + tick(400); + fixture.detectChanges(); + expect(overlayContainerElement.textContent).toContain('0'); + + dispatchClickEventSequence(sliderNativeElement, 0.13); + fixture.detectChanges(); + expect(overlayContainerElement.textContent).toContain('13'); + + // Always show tooltip even when handle is not hovered. + fixture.detectChanges(); + expect(overlayContainerElement.textContent).toContain('13'); + + tick(400); + })); + + it('should never display tooltips if set to `never`', fakeAsync(() => { + const handlerHost = sliderNativeElement.querySelector('nz-slider-handle'); + + testComponent.show = 'never'; + tick(400); + fixture.detectChanges(); + expect(overlayContainerElement.textContent).not.toContain('0'); + + dispatchClickEventSequence(sliderNativeElement, 0.13); + fixture.detectChanges(); + + // Do not show tooltip even when handle is hovered. + dispatchMouseEvent(handlerHost, 'mouseenter'); + fixture.detectChanges(); + expect(overlayContainerElement.textContent).not.toContain('13'); + })); }); describe('disabled slider', () => { @@ -487,7 +549,7 @@ describe('NzSlider', () => { overlayContainerElement = oc.getContainerElement(); })); - it('update the correct range value and show correct marks format', () => { + it('update the correct range value and show correct steps format', () => { expect(sliderNativeElement.textContent).toContain('(22%)'); expect(sliderNativeElement.textContent).toContain('(36%)'); @@ -523,7 +585,7 @@ describe('NzSlider', () => { expect(overlayContainerElement.textContent).toContain('VALUE-36'); }); - it('should stop at new marks when step=null or dots=true', () => { + it('should stop at new steps when step=null or dots=true', () => { testComponent.marks = { 15: { style: { 'color': 'red' }, label: '15' }, 33: '33' } as any; // tslint:disable-line:no-any testComponent.step = null; fixture.detectChanges(); @@ -552,7 +614,7 @@ describe('NzSlider', () => { expect(overlayContainerElement.textContent).toContain('VALUE-13'); dispatchMouseEvent(handlerHost, 'mouseleave'); - tick(400); // Wait for tooltip's antimations + tick(400); // Wait for tooltip's animations expect(overlayContainerElement.textContent).not.toContain('VALUE-13'); })); }); @@ -674,22 +736,26 @@ const styles = ` `; @Component({ - template: ``, - styles: [ styles ] + template: ` + `, + styles : [ styles ] }) -class StandardSliderComponent { } +class StandardSliderComponent { +} @Component({ - template: ``, - styles: [ styles ] + template: ` + `, + styles : [ styles ] }) class DisableSliderComponent { disable = true; } @Component({ - template: ``, - styles: [ styles ] + template: ` + `, + styles : [ styles ] }) class SliderWithMinAndMaxComponent { min = 4; @@ -697,40 +763,51 @@ class SliderWithMinAndMaxComponent { } @Component({ - template: ``, - styles: [ styles ] + template: ` + `, + styles : [ styles ] }) -class SliderWithValueComponent { } +class SliderWithValueComponent { +} @Component({ - template: ``, - styles: [ styles ] + template: ` + `, + styles : [ styles ] }) class SliderWithStepComponent { step = 25; } @Component({ - template: ``, - styles: [ styles ] + template: ` + `, + styles : [ styles ] }) -class SliderWithValueSmallerThanMinComponent { } +class SliderWithValueSmallerThanMinComponent { +} @Component({ - template: ``, - styles: [ styles ] + template: ` + `, + styles : [ styles ] }) -class SliderWithValueGreaterThanMaxComponent { } +class SliderWithValueGreaterThanMaxComponent { +} @Component({ - template: ``, - styles: [ styles ] + template: ` + `, + styles : [ styles ] }) -class VerticalSliderComponent { } +class VerticalSliderComponent { +} @Component({ - template: ``, - styles: [ styles ] + template: ` + `, + styles : [ styles ] }) class MixedSliderComponent { range = false; @@ -750,12 +827,13 @@ class MixedSliderComponent { `, - styles: [ styles ] + styles : [ styles ] }) class SliderWithFormControlComponent implements OnInit { form: FormGroup; - constructor(private fb: FormBuilder) { } + constructor(private fb: FormBuilder) { + } ngOnInit(): void { this.form = this.fb.group({ @@ -764,6 +842,16 @@ class SliderWithFormControlComponent implements OnInit { } } +@Component({ + template: ` + + ` +}) +class SliderShowTooltipComponent { + show: SliderShowTooltip = 'default'; + value = 0; +} + /** * Dispatches a click event sequence (consisting of moueseenter, click) from an element. * Note: The mouse event truncates the position for the click. diff --git a/components/slider/public-api.ts b/components/slider/public-api.ts index d24e18947a1..e74297ddceb 100644 --- a/components/slider/public-api.ts +++ b/components/slider/public-api.ts @@ -1,6 +1,5 @@ export * from './nz-slider.component'; export * from './nz-slider.module'; -export * from './nz-slider.service'; export * from './nz-slider-handle.component'; export * from './nz-slider-marks.component'; export * from './nz-slider-step.component'; diff --git a/components/timeline/nz-timeline.component.ts b/components/timeline/nz-timeline.component.ts index e32029b7fa4..73a1f4d13c0 100644 --- a/components/timeline/nz-timeline.component.ts +++ b/components/timeline/nz-timeline.component.ts @@ -18,7 +18,7 @@ import { import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -import { reverseChildNodes } from '../core/dom/reverse'; +import { reverseChildNodes } from '../core/util/dom'; import { NzTimelineItemComponent } from './nz-timeline-item.component'; export type NzTimelineMode = 'left' | 'alternate' | 'right';