1
// Styles
2 1
import './VRating.sass'
3

4
// Components
5 1
import VIcon from '../VIcon'
6

7
// Mixins
8 1
import Colorable from '../../mixins/colorable'
9 1
import Delayable from '../../mixins/delayable'
10 1
import Sizeable from '../../mixins/sizeable'
11 1
import Rippleable from '../../mixins/rippleable'
12 1
import Themeable from '../../mixins/themeable'
13

14
// Utilities
15 1
import { createRange } from '../../util/helpers'
16 1
import mixins from '../../util/mixins'
17

18
// Types
19
import { VNode, VNodeDirective, VNodeChildren } from 'vue'
20

21
type ItemSlotProps = {
22
  index: number
23
  value: number
24
  isFilled: boolean
25
  isHalfFilled?: boolean | undefined
26
  isHovered: boolean
27
  isHalfHovered?: boolean | undefined
28
  click: Function
29
}
30

31
/* @vue/component */
32 1
export default mixins(
33
  Colorable,
34
  Delayable,
35
  Rippleable,
36
  Sizeable,
37
  Themeable
38
).extend({
39
  name: 'v-rating',
40

41
  props: {
42
    backgroundColor: {
43
      type: String,
44
      default: 'accent',
45
    },
46
    color: {
47
      type: String,
48
      default: 'primary',
49
    },
50
    clearable: Boolean,
51
    dense: Boolean,
52
    emptyIcon: {
53
      type: String,
54
      default: '$ratingEmpty',
55
    },
56
    fullIcon: {
57
      type: String,
58
      default: '$ratingFull',
59
    },
60
    halfIcon: {
61
      type: String,
62
      default: '$ratingHalf',
63
    },
64
    halfIncrements: Boolean,
65
    hover: Boolean,
66
    length: {
67
      type: [Number, String],
68
      default: 5,
69
    },
70
    readonly: Boolean,
71
    size: [Number, String],
72
    value: {
73
      type: Number,
74
      default: 0,
75
    },
76
  },
77

78 1
  data () {
79 1
    return {
80
      hoverIndex: -1,
81
      internalValue: this.value,
82
    }
83
  },
84

85
  computed: {
86 1
    directives (): VNodeDirective[] {
87 1
      if (this.readonly || !this.ripple) return []
88

89 1
      return [{
90
        name: 'ripple',
91
        value: { circle: true },
92
      } as VNodeDirective]
93
    },
94 1
    iconProps (): object {
95
      const {
96 1
        dark,
97 1
        large,
98 1
        light,
99 1
        medium,
100 1
        small,
101 1
        size,
102 1
        xLarge,
103 1
        xSmall,
104 1
      } = this.$props
105

106 1
      return {
107
        dark,
108
        large,
109
        light,
110
        medium,
111
        size,
112
        small,
113
        xLarge,
114
        xSmall,
115
      }
116
    },
117 1
    isHovering (): boolean {
118 1
      return this.hover && this.hoverIndex >= 0
119
    },
120
  },
121

122
  watch: {
123 1
    internalValue (val) {
124 1
      val !== this.value && this.$emit('input', val)
125
    },
126 1
    value (val) {
127 1
      this.internalValue = val
128
    },
129
  },
130

131
  methods: {
132 1
    createClickFn (i: number): Function {
133 1
      return (e: MouseEvent) => {
134 1
        if (this.readonly) return
135

136 1
        const newValue = this.genHoverIndex(e, i)
137 1
        if (this.clearable && this.internalValue === newValue) {
138 1
          this.internalValue = 0
139
        } else {
140 1
          this.internalValue = newValue
141
        }
142
      }
143
    },
144 1
    createProps (i: number): ItemSlotProps {
145 1
      const props: ItemSlotProps = {
146
        index: i,
147
        value: this.internalValue,
148
        click: this.createClickFn(i),
149
        isFilled: Math.floor(this.internalValue) > i,
150
        isHovered: Math.floor(this.hoverIndex) > i,
151
      }
152

153 1
      if (this.halfIncrements) {
154 1
        props.isHalfHovered = !props.isHovered && (this.hoverIndex - i) % 1 > 0
155 1
        props.isHalfFilled = !props.isFilled && (this.internalValue - i) % 1 > 0
156
      }
157

158 1
      return props
159
    },
160 1
    genHoverIndex (e: MouseEvent, i: number) {
161 1
      let isHalf = this.isHalfEvent(e)
162

163 1
      if (
164 1
        this.halfIncrements &&
165 1
        this.$vuetify.rtl
166
      ) {
167 1
        isHalf = !isHalf
168
      }
169

170 1
      return i + (isHalf ? 0.5 : 1)
171
    },
172 1
    getIconName (props: ItemSlotProps): string {
173 1
      const isFull = this.isHovering ? props.isHovered : props.isFilled
174 1
      const isHalf = this.isHovering ? props.isHalfHovered : props.isHalfFilled
175

176 1
      return isFull ? this.fullIcon : isHalf ? this.halfIcon : this.emptyIcon
177
    },
178 1
    getColor (props: ItemSlotProps): string {
179 1
      if (this.isHovering) {
180 1
        if (props.isHovered || props.isHalfHovered) return this.color
181
      } else {
182 1
        if (props.isFilled || props.isHalfFilled) return this.color
183
      }
184

185 1
      return this.backgroundColor
186
    },
187 1
    isHalfEvent (e: MouseEvent): boolean {
188 1
      if (this.halfIncrements) {
189 1
        const rect = e.target && (e.target as HTMLElement).getBoundingClientRect()
190 1
        if (rect && (e.pageX - rect.left) < rect.width / 2) return true
191
      }
192

193 1
      return false
194
    },
195 1
    onMouseEnter (e: MouseEvent, i: number): void {
196 1
      this.runDelay('open', () => {
197 1
        this.hoverIndex = this.genHoverIndex(e, i)
198
      })
199
    },
200 1
    onMouseLeave (): void {
201 1
      this.runDelay('close', () => (this.hoverIndex = -1))
202
    },
203 1
    genItem (i: number): VNode | VNodeChildren | string {
204 1
      const props = this.createProps(i)
205

206 1
      if (this.$scopedSlots.item) return this.$scopedSlots.item(props)
207

208 1
      const listeners: Record<string, Function> = {
209
        click: props.click,
210
      }
211

212 1
      if (this.hover) {
213 1
        listeners.mouseenter = (e: MouseEvent) => this.onMouseEnter(e, i)
214 1
        listeners.mouseleave = this.onMouseLeave
215

216 1
        if (this.halfIncrements) {
217 1
          listeners.mousemove = (e: MouseEvent) => this.onMouseEnter(e, i)
218
        }
219
      }
220

221 1
      return this.$createElement(VIcon, this.setTextColor(this.getColor(props), {
222
        attrs: { tabindex: -1 }, // TODO: Add a11y support
223
        directives: this.directives,
224
        props: this.iconProps,
225
        on: listeners,
226
      }), [this.getIconName(props)])
227
    },
228
  },
229

230 1
  render (h): VNode {
231 1
    const children = createRange(Number(this.length)).map(i => this.genItem(i))
232

233 1
    return h('div', {
234
      staticClass: 'v-rating',
235
      class: {
236
        'v-rating--readonly': this.readonly,
237
        'v-rating--dense': this.dense,
238
      },
239
    }, children)
240
  },
241
})

Read our documentation on viewing source code .

Loading