diff --git a/src/rate/index.js b/src/rate/index.js new file mode 100644 index 000000000..4430683ea --- /dev/null +++ b/src/rate/index.js @@ -0,0 +1,199 @@ +import { createNamespace, addUnit } from '../utils'; +import { preventDefault } from '../utils/dom/event'; +import { TouchMixin } from '../mixins/touch'; +import Icon from '../icon'; + +const [createComponent, bem] = createNamespace('rate'); + +function getRateStatus(value, index, allowHalf) { + if (value >= index) { + return 'full'; + } + + if (value + 0.5 >= index && allowHalf) { + return 'half'; + } + + return 'void'; +} + +export default createComponent({ + mixins: [TouchMixin], + + props: { + size: [Number, String], + gutter: [Number, String], + readonly: Boolean, + disabled: Boolean, + allowHalf: Boolean, + value: { + type: Number, + default: 0 + }, + icon: { + type: String, + default: 'star' + }, + voidIcon: { + type: String, + default: 'star-o' + }, + color: { + type: String, + default: '#ffd21e' + }, + voidColor: { + type: String, + default: '#c7c7c7' + }, + disabledColor: { + type: String, + default: '#bdbdbd' + }, + count: { + type: Number, + default: 5 + } + }, + + computed: { + list() { + const list = []; + for (let i = 1; i <= this.count; i++) { + list.push(getRateStatus(this.value, i, this.allowHalf)); + } + + return list; + }, + + sizeWithUnit() { + return addUnit(this.size); + }, + + gutterWithUnit() { + return addUnit(this.gutter); + } + }, + + methods: { + select(index) { + if (!this.disabled && !this.readonly && index !== this.value) { + this.$emit('input', index); + this.$emit('change', index); + } + }, + + onTouchStart(event) { + if (this.readonly || this.disabled) { + return; + } + + this.touchStart(event); + + const rects = this.$refs.items.map(item => item.getBoundingClientRect()); + const ranges = []; + + rects.forEach((rect, index) => { + if (this.allowHalf) { + ranges.push( + { score: index + 0.5, left: rect.left }, + { score: index + 1, left: rect.left + rect.width / 2 } + ); + } else { + ranges.push({ score: index + 1, left: rect.left }); + } + }); + + this.ranges = ranges; + }, + + onTouchMove(event) { + if (this.readonly || this.disabled) { + return; + } + + this.touchMove(event); + + if (this.direction === 'horizontal') { + preventDefault(event); + + const { clientX } = event.touches[0]; + this.select(this.getScoreByPosition(clientX)); + } + }, + + getScoreByPosition(x) { + for (let i = this.ranges.length - 1; i > 0; i--) { + if (x > this.ranges[i].left) { + return this.ranges[i].score; + } + } + + return this.allowHalf ? 0.5 : 1; + }, + + renderStar(status, index) { + const { icon, color, count, voidIcon, disabled, voidColor, disabledColor } = this; + const score = index + 1; + const isFull = status === 'full'; + const isVoid = status === 'void'; + + let style; + if (this.gutterWithUnit && score !== count) { + style = { paddingRight: this.gutterWithUnit }; + } + + return ( + + ); + } + }, + + render() { + return ( +
+ {this.list.map((status, index) => this.renderStar(status, index))} +
+ ); + } +}); diff --git a/src/rate/index.tsx b/src/rate/index.tsx deleted file mode 100644 index e271e5e09..000000000 --- a/src/rate/index.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import { createNamespace, addUnit } from '../utils'; -import { emit, inherit } from '../utils/functional'; -import { preventDefault } from '../utils/dom/event'; -import Icon from '../icon'; - -// Types -import { CreateElement, RenderContext } from 'vue/types'; -import { DefaultSlots } from '../utils/types'; - -const [createComponent, bem] = createNamespace('rate'); - -export type RateProps = { - size: number; - icon: string; - count: number; - color: string; - value: number; - gutter?: string | number; - voidIcon: string; - voidColor: string; - readonly?: boolean; - disabled?: boolean; - allowHalf?: boolean; - disabledColor: string; -}; - -export type RateEvents = { - input(index: number): void; - change(index: number): void; -}; - -type RateStatus = 'full' | 'half' | 'void'; - -function getRateStatus(value: number, index: number, allowHalf?: boolean): RateStatus { - if (value >= index) { - return 'full'; - } - if (value + 0.5 >= index && allowHalf) { - return 'half'; - } - return 'void'; -} - -function Rate( - h: CreateElement, - props: RateProps, - slots: DefaultSlots, - ctx: RenderContext -) { - const { - icon, - color, - count, - voidIcon, - readonly, - disabled, - voidColor, - disabledColor - } = props; - - const list: RateStatus[] = []; - for (let i = 1; i <= count; i++) { - list.push(getRateStatus(props.value, i, props.allowHalf)); - } - - function onSelect(index: number) { - if (!disabled && !readonly) { - emit(ctx, 'input', index); - emit(ctx, 'change', index); - } - } - - function onTouchMove(event: TouchEvent) { - if (readonly || disabled || !document.elementFromPoint) { - return; - } - - preventDefault(event); - const { clientX, clientY } = event.touches[0]; - const target = document.elementFromPoint(clientX, clientY) as HTMLElement; - - if (target && target.dataset) { - const { score } = target.dataset; - - /* istanbul ignore else */ - if (score) { - onSelect(+score); - } - } - } - - const gutter = addUnit(props.gutter); - - function renderStar(status: RateStatus, index: number) { - const isFull = status === 'full'; - const isVoid = status === 'void'; - const score = index + 1; - const size = addUnit(props.size); - - let style; - if (gutter && score !== count) { - style = { paddingRight: gutter }; - } - - return ( - - ); - } - - return ( -
- {list.map((status, index) => renderStar(status, index))} -
- ); -} - -Rate.props = { - size: [Number, String], - gutter: [Number, String], - readonly: Boolean, - disabled: Boolean, - allowHalf: Boolean, - value: { - type: Number, - default: 0 - }, - icon: { - type: String, - default: 'star' - }, - voidIcon: { - type: String, - default: 'star-o' - }, - color: { - type: String, - default: '#ffd21e' - }, - voidColor: { - type: String, - default: '#c7c7c7' - }, - disabledColor: { - type: String, - default: '#bdbdbd' - }, - count: { - type: Number, - default: 5 - } -}; - -export default createComponent(Rate); diff --git a/src/rate/test/index.spec.js b/src/rate/test/index.spec.js index 5cc8229ce..b55315c31 100644 --- a/src/rate/test/index.spec.js +++ b/src/rate/test/index.spec.js @@ -1,23 +1,38 @@ import Rate from '..'; import { mount, triggerDrag } from '../../../test/utils'; +function mockGetBoundingClientRect(items) { + items.filter((icon, index) => { + icon.element.getBoundingClientRect = () => ({ + left: index * 25, + width: 25 + }); + return true; + }); +} + test('change event', () => { const onInput = jest.fn(); const onChange = jest.fn(); const wrapper = mount(Rate, { - context: { - on: { - input: onInput, - change: onChange - } + listeners: { + input: value => { + onInput(value); + wrapper.setProps({ value }); + }, + change: onChange } }); const item4 = wrapper.findAll('.van-rate__icon').at(3); item4.trigger('click'); + item4.trigger('click'); + expect(onInput).toHaveBeenCalledWith(4); + expect(onInput).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith(4); + expect(onChange).toHaveBeenCalledTimes(1); }); test('allow half', () => { @@ -28,11 +43,9 @@ test('allow half', () => { propsData: { allowHalf: true }, - context: { - on: { - input: onInput, - change: onChange - } + listeners: { + input: onInput, + change: onChange } }); const item4 = wrapper.findAll('.van-rate__icon--half').at(3); @@ -50,43 +63,59 @@ test('disabled', () => { propsData: { disabled: true }, - context: { - on: { - input: onInput, - change: onChange - } + listeners: { + input: onInput, + change: onChange } }); const item4 = wrapper.findAll('.van-rate__item').at(3); + triggerDrag(wrapper, 100, 0); item4.trigger('click'); + expect(onInput).toHaveBeenCalledTimes(0); expect(onChange).toHaveBeenCalledTimes(0); }); -test('touchmove', () => { +test('touchmove to select item', () => { const onChange = jest.fn(); const wrapper = mount(Rate, { - context: { - on: { - change: onChange - } + listeners: { + change: onChange } }); + + const icons = wrapper.findAll('.van-rate__item'); + + mockGetBoundingClientRect(icons); triggerDrag(wrapper, 100, 0); - const icons = wrapper.findAll('.van-icon'); - document.elementFromPoint = function (x) { - const index = Math.round(x / 20); - if (index < icons.length) { - return icons.at(index).element; + expect(onChange).toHaveBeenNthCalledWith(1, 1); + expect(onChange).toHaveBeenNthCalledWith(2, 2); + expect(onChange).toHaveBeenNthCalledWith(3, 2); + expect(onChange).toHaveBeenNthCalledWith(4, 4); +}); + +test('touchmove to select half item', () => { + const onChange = jest.fn(); + const wrapper = mount(Rate, { + propsData: { + allowHalf: true + }, + listeners: { + change: onChange } - }; + }); + const icons = wrapper.findAll('.van-rate__item'); + + mockGetBoundingClientRect(icons); triggerDrag(wrapper, 100, 0); - expect(onChange).toHaveBeenNthCalledWith(1, 2); - expect(onChange).toHaveBeenNthCalledWith(2, 3); - expect(onChange).toHaveBeenNthCalledWith(3, 4); + + expect(onChange).toHaveBeenNthCalledWith(1, 1); + expect(onChange).toHaveBeenNthCalledWith(2, 1.5); + expect(onChange).toHaveBeenNthCalledWith(3, 2); + expect(onChange).toHaveBeenNthCalledWith(4, 4); }); test('gutter prop', () => {