@rdkmaster/jigsaw-labs
Version:
Jigsaw, the next generation component set for RDK
549 lines (461 loc) • 15.4 kB
text/typescript
/**
* Created by 10177553 on 2017/4/13.
*/
import {
Component,
ElementRef,
EventEmitter, forwardRef, Host, Inject,
Input, NgZone,
OnDestroy,
OnInit,
Output,
QueryList,
Renderer2,
ViewChildren,
ViewEncapsulation
} from "@angular/core";
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from "@angular/forms";
import {CommonUtils} from "../../core/utils/common-utils";
import {ArrayCollection} from "../../core/data/array-collection";
import {CallbackRemoval} from "../../core/utils/common-utils";
import {AbstractJigsawComponent} from "../common";
export class SliderMark {
value: number;
label: string;
style?: any;
}
/**
* @internal
*/
export class JigsawSliderHandle implements OnInit {
private _value: number;
public key: number;
public get value() {
return this._value;
}
public set value(value) {
this._value = this._slider._verifyValue(value);
this._valueToPos();
}
public change = new EventEmitter<number>();
private _valueToPos() {
this._offset = this._slider._transformValueToPos(this.value);
this.setHandleStyle();
}
private _offset: number = 0;
/**
* @internal
*/
public _$handleStyle = {};
private setHandleStyle() {
if (isNaN(this._offset)) return;
this._zone.runOutsideAngular(() => {
if (this._slider.vertical) { // 兼容垂直滑动条;
this._$handleStyle = {
bottom: this._offset + "%"
}
} else {
this._$handleStyle = {
left: this._offset + "%"
}
}
})
}
private _dragging: boolean = false;
public transformPosToValue(pos) {
// 更新取得的滑动条尺寸.
this._slider._refresh();
let dimensions = this._slider._dimensions;
// bottom 在dom中的位置.
let offset = this._slider.vertical ? dimensions.bottom : dimensions.left;
let size = this._slider.vertical ? dimensions.height : dimensions.width;
let posValue = this._slider.vertical ? pos.y - 6 : pos.x;
if (this._slider.vertical) {
posValue = posValue > offset ? offset : posValue;
} else {
posValue = posValue < offset ? offset : posValue;
}
let newValue = Math.abs(posValue - offset) / size * (this._slider.max - this._slider.min) + (this._slider.min - 0); // 保留两位小数
let m = this._calFloat(this._slider.step);
// 解决出现的有时小数点多了N多位.
newValue = Math.round(Math.round(newValue / this._slider.step) * this._slider.step * Math.pow(10, m)) / Math.pow(10, m);
return this._slider._verifyValue(newValue);
}
/**
* 计算需要保留小数的位数
* 子级组件需要用到
* @internal
*/
public _calFloat(value: number): number {
// 增加步长的计算;
let m = 0;
try {
m = this._slider.step.toString().split(".")[1].length;
} catch (e) {
}
return m;
}
/**
* @internal
*/
public _$startToDrag() {
this._dragging = true;
this._registerGlobalEvent();
}
globalEventMouseMove: Function;
globalEventMouseUp: Function;
_registerGlobalEvent() {
this.globalEventMouseMove = this._render.listen("document", "mousemove", (e) => {
this._$updateValuePosition(e);
});
this.globalEventMouseUp = this._render.listen("document", "mouseup", () => {
this._dragging = false;
this._destroyGlobalEvent();
});
}
_destroyGlobalEvent() {
if (this.globalEventMouseMove) {
this.globalEventMouseMove();
}
if (this.globalEventMouseUp) {
this.globalEventMouseUp();
}
}
private _slider: JigsawSlider; // 父组件;
constructor(private _render: Renderer2, slider: JigsawSlider, private _zone: NgZone) {
this._slider = slider;
}
/**
* 改变value的值
* @internal
*/
public _$updateValuePosition(event?) {
if (!this._dragging || this._slider.disabled) return;
// 防止产生选中其他文本,造成鼠标放开后还可以拖拽的奇怪现象;
event.stopPropagation();
event.preventDefault();
let pos = {
x: event["clientX"],
y: event["clientY"]
};
let newValue = this.transformPosToValue(pos);
if (this.value === newValue) return;
this.value = newValue;
this._slider._updateValue(this.key, newValue);
}
ngOnInit() {
this._valueToPos();
}
}
/**
* @description 滑动条组件.
*
* 何时使用
* 当用户需要在数值区间/自定义区间内进行选择时
*/
export class JigsawSlider extends AbstractJigsawComponent implements ControlValueAccessor, OnInit, OnDestroy {
constructor(private _element: ElementRef, private _render: Renderer2, private _zone: NgZone) {
super();
}
public valid: boolean = true;
// Todo 支持滑动条点击.
private _sliderHandle: QueryList<JigsawSliderHandle>;
/**
* @internal
*/
public _$value: ArrayCollection<number> = new ArrayCollection<number>();
private _removeRefreshCallback: CallbackRemoval = this._getRemoveRefreshCallback();
/**
* slider的当前值, 类型 number | ArrayCollection<number> 支持多触点.
* @returns {any}
*/
public get value(): number | ArrayCollection<number> {
// 兼容返回单个值, 和多触点的数组;
if (this._$value.length == 1) {
return this._$value[0];
} else {
return this._$value;
}
}
public set value(value: number | ArrayCollection<number>) {
this.writeValue(value);
}
/**
* 此函数用于ngFor比较数组的值更新视图时,使数组不管值是否相等都能够更新
* @internal
* @param index
* @param item
* @private
*/
public _$trackByFun = function(index: number, item: any) {
return index + new Date().getTime()
};
/**
* 设置单个的值。内部使用
* 子级组件需要用到
* @internal
*/
public _updateValue(index: number, value: number) {
this._$value.set(index, value);
this._$value.refresh();
}
/**
* 最后重新计算一下,垂直滚动条的位置
* 子级组件需要用到
* @internal
*/
public _refresh() {
this._dimensions = this._element.nativeElement.getBoundingClientRect();
}
/**
* 使 value 支持双向绑定
* @type {EventEmitter<number|ArrayCollection<number>>}
*/
public valueChange = new EventEmitter<number | ArrayCollection<number>>();
// 当滑动条的组件值变化时,对外发出的事件
public change = this.valueChange;
private _min: number = 0;
/**
* 可选范围的最小值
* @returns {number}
*/
public get min() {
return this._min;
}
public set min(min: number) {
this._min = min;
}
private _max: number = 100;
/**
* 输入范围的可选最大值.
* @returns {number}
*/
public get max() {
return this._max;
}
public set max(max: number) {
this._max = max;
}
private _step: number = 1;
/**
* 每次变化的最小值, 最小支持小数点后两位.
* @returns {number}
*/
public get step() {
return this._step;
}
public set step(value: number) {
this._step = value;
}
/**
* 子级组件需要用到
* @internal
*/
public _transformValueToPos(value?) {
// 检验值的合法性, 不合法转换成默认可接受的合法值;
value = this._verifyValue(value);
return (value - this.min) / (this.max - this.min) * 100;
}
/**
* 子级组件需要用到
* @internal
*/
public _dimensions: ClientRect;
/**
* 垂直滑动条 默认 false
* @type {boolean}
*/
public vertical: boolean = false;
/**
* 是否禁用. 数据类型 boolean, 默认false;
* @type {boolean}
*/
public disabled: boolean = false;
/**
* @internal
*/
public _$trackStyle = {};
private _setTrackStyle(value?) {
// 兼容双触点.
let startPos: number = 0;
let trackSize: number = CommonUtils.isDefined(value) ? this._transformValueToPos(value) : this._transformValueToPos(this.value); // 默认单触点位置
if (this._$value.length > 1) {
let max: number = this._$value[0];
let min: number = this._$value[0];
this._$value.map(item => {
if (max - item < 0) max = item;
else if (item - min < 0) min = item;
});
startPos = this._transformValueToPos(min);
trackSize = Math.abs(this._transformValueToPos(max) - this._transformValueToPos(min));
}
if (this.vertical) { // 垂直和水平两种
this._$trackStyle = {
bottom: startPos + "%",
height: trackSize + "%"
}
} else {
this._$trackStyle = {
left: startPos + "%",
width: trackSize + "%"
}
}
}
/**
* @internal
*/
public _$marks: any[] = [];
private _marks: SliderMark[];
/**
* marks 标签 使用格式为 [Object] 其中 Object 必须包含value 及label 可以有style 属性 例如: marks = [{value: 20, label: '20 ℃'},
*/
public get marks(): SliderMark[] {
return this._marks;
}
public set marks(value: SliderMark[]) {
this._marks = value;
this._calcMarks();
}
private _calcMarks() {
if (!this._marks || !this.initialized) return;
this._$marks.splice(0, this._$marks.length);
let size = Math.round(100 / this._marks.length);
let margin = -Math.floor(size / 2);
let vertical = this.vertical;
this._marks.forEach(mark => {
const richMark: any = {};
if (vertical) {
richMark.dotStyle = {
bottom: this._transformValueToPos(mark.value) + "%"
};
richMark.labelStyle = {
bottom: this._transformValueToPos(mark.value) + "%",
"margin-bottom": margin + "%"
};
} else {
richMark.dotStyle = {
top: "-2px",
left: this._transformValueToPos(mark.value) + "%"
};
richMark.labelStyle = {
left: this._transformValueToPos(mark.value) + "%",
width: size + "%", "margin-left": margin + "%"
};
}
// 如果用户自定义了样式, 要进行样式的合并;
CommonUtils.extendObject(richMark.labelStyle, mark.style);
richMark.label = mark.label;
this._$marks.push(richMark);
});
}
ngOnInit() {
super.ngOnInit();
// 计算slider 的尺寸.
this._dimensions = this._element.nativeElement.getBoundingClientRect();
// 设置标记.
this._calcMarks();
// 注册resize事件;
this.resize();
}
private _removeResizeEvent: Function;
private resize() {
this._zone.runOutsideAngular(() => {
this._removeResizeEvent = this._render.listen("window", "resize", () => {
// 计算slider 的尺寸.
this._dimensions = this._element.nativeElement.getBoundingClientRect();
})
})
}
/**
* 暂没有使用场景.
*/
public ngOnDestroy() {
if (this._removeResizeEvent) {
this._removeResizeEvent();
}
if (this._removeRefreshCallback) {
this._removeRefreshCallback()
}
}
/**
* 校验value的合法性. 大于最大值,取最大值, 小于最小值取最小值
* 子级组件需要用到
* @internal
*/
public _verifyValue(value: number) {
if (value - this.min < 0 && this.initialized) {
return this.min;
} else if (value - this.max > 0 && this.initialized) {
return this.max;
} else {
return value;
}
}
private _getRemoveRefreshCallback() {
return this._$value.onRefresh(() => {
this._zone.runOutsideAngular(() => this._setTrackStyle(this.value));
this.valueChange.emit(this.value);
this._propagateChange(this.value);
});
}
private _propagateChange: any = () => {
};
// ngModel触发的writeValue方法,只会在ngOnInit,ngAfterContentInit,ngAfterViewInit这些生命周期之后才调用
public writeValue(value: any): void {
if(value instanceof Array) {
value = new ArrayCollection(value);
}
if (value instanceof ArrayCollection) {
if (this._$value !== value) {
this._$value = value;
if (this._removeRefreshCallback) {
this._removeRefreshCallback();
}
this._removeRefreshCallback = this._getRemoveRefreshCallback();
}
} else {
this._$value.splice(0, this._$value.length);
this._$value.push(this._verifyValue(+value));
}
// refresh的回调是异步的
this._$value.refresh();
}
public registerOnChange(fn: any): void {
this._propagateChange = fn;
}
public registerOnTouched(fn: any): void {
}
}