@coreui/coreui-pro
Version:
The most popular front-end framework for developing responsive, mobile-first projects on the web rewritten by the CoreUI Team
528 lines (422 loc) • 16.8 kB
JavaScript
/**
* --------------------------------------------------------------------------
* CoreUI PRO rating.js
* License (https://coreui.io/pro/license/)
* --------------------------------------------------------------------------
*/
import BaseComponent from './base-component.js'
import EventHandler from './dom/event-handler.js'
import Manipulator from './dom/manipulator.js'
import SelectorEngine from './dom/selector-engine.js'
import { DefaultAllowlist, sanitizeHtml } from './util/sanitizer.js'
import { defineJQueryPlugin, getUID } from './util/index.js'
import Tooltip from './tooltip.js'
/**
* Constants
*/
const NAME = 'rating'
const DATA_KEY = 'coreui.rating'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn'])
const EVENT_CHANGE = `change${EVENT_KEY}`
const EVENT_CLICK = `click${EVENT_KEY}`
const EVENT_FOCUSIN = `focusin${EVENT_KEY}`
const EVENT_FOCUSOUT = `focusout${EVENT_KEY}`
const EVENT_HOVER = `hover${EVENT_KEY}`
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
const EVENT_MOUSEENTER = `mouseenter${EVENT_KEY}`
const EVENT_MOUSELEAVE = `mouseleave${EVENT_KEY}`
const CLASS_NAME_ACTIVE = 'active'
const CLASS_NAME_DISABLED = 'disabled'
const CLASS_NAME_RATING = 'rating'
const CLASS_NAME_RATING_ITEM = 'rating-item'
const CLASS_NAME_RATING_ITEM_ICON = 'rating-item-icon'
const CLASS_NAME_RATING_ITEM_CUSTOM_ICON = 'rating-item-custom-icon'
const CLASS_NAME_RATING_ITEM_CUSTOM_ICON_ACTIVE = 'rating-item-custom-icon-active'
const CLASS_NAME_RATING_ITEM_INPUT = 'rating-item-input'
const CLASS_NAME_RATING_ITEM_LABEL = 'rating-item-label'
const CLASS_NAME_READONLY = 'readonly'
const SELECTOR_DATA_TOGGLE = '[data-coreui-toggle="rating"]'
const SELECTOR_RATING_ITEM_INPUT = '.rating-item-input'
const SELECTOR_RATING_ITEM_LABEL = '.rating-item-label'
// js-docs-start svg-allow-list
export const svgAllowList = {
...DefaultAllowlist,
svg: ['xmlns', 'version', 'baseprofile', 'width', 'height', 'viewbox', 'preserveaspectratio', 'aria-hidden', 'role', 'focusable'],
g: ['id', 'class', 'transform', 'style'],
path: ['id', 'class', 'd', 'fill', 'fill-opacity', 'fill-rule', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity'],
circle: ['id', 'class', 'cx', 'cy', 'r', 'fill', 'fill-opacity', 'stroke', 'stroke-width', 'stroke-opacity'],
rect: ['id', 'class', 'x', 'y', 'width', 'height', 'rx', 'ry', 'fill', 'fill-opacity', 'stroke', 'stroke-width', 'stroke-opacity'],
ellipse: ['id', 'class', 'cx', 'cy', 'rx', 'ry', 'fill', 'fill-opacity', 'stroke', 'stroke-width', 'stroke-opacity'],
line: ['id', 'class', 'x1', 'y1', 'x2', 'y2', 'stroke', 'stroke-width', 'stroke-opacity'],
polygon: ['id', 'class', 'points', 'fill', 'fill-opacity', 'stroke', 'stroke-width', 'stroke-opacity'],
polyline: ['id', 'class', 'points', 'fill', 'fill-opacity', 'stroke', 'stroke-width', 'stroke-opacity'],
text: ['id', 'class', 'x', 'y', 'dx', 'dy', 'text-anchor', 'font-family', 'font-size', 'font-weight', 'fill', 'fill-opacity', 'stroke', 'stroke-width', 'stroke-opacity'],
tspan: ['id', 'class', 'x', 'y', 'dx', 'dy', 'text-anchor', 'font-family', 'font-size', 'font-weight', 'fill', 'fill-opacity', 'stroke', 'stroke-width', 'stroke-opacity'],
defs: [],
symbol: ['id', 'class', 'viewbox', 'preserveaspectratio'],
use: ['id', 'class', 'x', 'y', 'width', 'height', 'href'],
image: ['id', 'class', 'x', 'y', 'width', 'height', 'href', 'preserveaspectratio', 'xlink:href'],
pattern: ['id', 'class', 'x', 'y', 'width', 'height', 'patternunits', 'patterncontentunits', 'patterntransform', 'preserveaspectratio'],
lineargradient: ['id', 'class', 'gradientunits', 'x1', 'y1', 'x2', 'y2', 'spreadmethod', 'gradienttransform'],
radialgradient: ['id', 'class', 'gradientunits', 'cx', 'cy', 'r', 'fx', 'fy', 'spreadmethod', 'gradienttransform'],
mask: ['id', 'class', 'x', 'y', 'width', 'height', 'maskunits', 'maskcontentunits', 'masktransform'],
clippath: ['id', 'class', 'clippathunits'],
marker: ['id', 'class', 'markerunits', 'markerwidth', 'markerheight', 'orient', 'preserveaspectratio', 'viewbox', 'refx', 'refy'],
title: [],
desc: []
}
// js-docs-end svg-allow-list
const Default = {
activeIcon: null,
allowClear: false,
allowList: svgAllowList,
disabled: false,
highlightOnlySelected: false,
icon: null,
itemCount: 5,
name: null,
precision: 1,
readOnly: false,
sanitize: true,
sanitizeFn: null,
size: null,
tooltips: false,
value: null
}
const DefaultType = {
activeIcon: '(object|string|null)',
allowClear: 'boolean',
allowList: 'object',
disabled: 'boolean',
highlightOnlySelected: 'boolean',
icon: '(object|string|null)',
itemCount: 'number',
name: '(string|null)',
precision: 'number',
readOnly: 'boolean',
sanitize: 'boolean',
sanitizeFn: '(null|function)',
size: '(string|null)',
tooltips: '(array|boolean|object)',
value: '(number|null)'
}
/**
* Class definition
*/
class Rating extends BaseComponent {
constructor(element, config) {
super(element)
this._config = this._getConfig(config)
this._currentValue = this._config.value
this._name = this._config.name || getUID(`${this.constructor.NAME}-name-`).toString()
this._tooltip = null
this._createRating()
this._addEventListeners()
}
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
update(config) {
this._config = this._getConfig(config)
this._currentValue = this._config.value
this._element.innerHTML = ''
this._createRating()
this._addEventListeners()
}
reset(value = null) {
this._currentValue = value
this._element.innerHTML = ''
this._createRating()
this._addEventListeners()
EventHandler.trigger(this._element, EVENT_CHANGE, {
value
})
}
// Private
_addEventListeners() {
EventHandler.on(this._element, EVENT_CLICK, SELECTOR_RATING_ITEM_INPUT, ({ target }) => {
if (this._config.disabled || this._config.readOnly) {
return
}
// eslint-disable-next-line eqeqeq
if (this._config.allowClear && this._currentValue == target.value) {
this._currentValue = null
target.checked = false
this._resetLabels()
EventHandler.trigger(this._element, EVENT_CHANGE, {
value: null
})
}
})
EventHandler.on(this._element, EVENT_CHANGE, SELECTOR_RATING_ITEM_INPUT, ({ target }) => {
if (this._config.disabled || this._config.readOnly) {
return
}
this._currentValue = target.value
EventHandler.trigger(this._element, EVENT_CHANGE, {
value: target.value
})
const inputs = SelectorEngine.find(SELECTOR_RATING_ITEM_INPUT, this._element)
this._resetLabels()
if (this._config.highlightOnlySelected) {
const label = SelectorEngine.findOne(SELECTOR_RATING_ITEM_LABEL, target.parentElement)
label.classList.add(CLASS_NAME_ACTIVE)
return
}
for (const input of inputs) {
const label = SelectorEngine.findOne(SELECTOR_RATING_ITEM_LABEL, input.parentElement)
label.classList.add(CLASS_NAME_ACTIVE)
if (input === target) {
break
}
}
})
EventHandler.on(this._element, EVENT_MOUSEENTER, SELECTOR_RATING_ITEM_LABEL, ({ target }) => {
if (this._config.disabled || this._config.readOnly) {
return
}
const label = target.closest(SELECTOR_RATING_ITEM_LABEL)
const labels = SelectorEngine.find(SELECTOR_RATING_ITEM_LABEL, this._element)
this._resetLabels()
const input = SelectorEngine.findOne(SELECTOR_RATING_ITEM_INPUT, label.parentElement)
EventHandler.trigger(this._element, EVENT_HOVER, {
value: input.value
})
this._createTooltip(label.parentElement, input.value)
if (this._config.highlightOnlySelected) {
label.classList.add(CLASS_NAME_ACTIVE)
return
}
for (const _label of labels) {
_label.classList.add(CLASS_NAME_ACTIVE)
if (_label === label) {
break
}
}
})
EventHandler.on(this._element, EVENT_MOUSELEAVE, SELECTOR_RATING_ITEM_LABEL, () => {
if (this._config.disabled || this._config.readOnly) {
return
}
if (this._tooltip) {
this._tooltip.hide()
}
const checkedInput = SelectorEngine.findOne(`${SELECTOR_RATING_ITEM_INPUT}[value="${this._currentValue}"]`, this._element)
this._resetLabels()
EventHandler.trigger(this._element, EVENT_HOVER, {
value: null
})
if (checkedInput && this._config.highlightOnlySelected) {
const label = SelectorEngine.findOne(SELECTOR_RATING_ITEM_LABEL, checkedInput.parentElement)
label.classList.add(CLASS_NAME_ACTIVE)
return
}
if (checkedInput) {
const inputs = SelectorEngine.find(SELECTOR_RATING_ITEM_INPUT, this._element)
this._resetLabels()
for (const input of inputs) {
const label = SelectorEngine.findOne(SELECTOR_RATING_ITEM_LABEL, input.parentElement)
label.classList.add(CLASS_NAME_ACTIVE)
if (input === checkedInput) {
break
}
}
}
})
EventHandler.on(this._element, EVENT_FOCUSIN, SELECTOR_RATING_ITEM_INPUT, ({ target }) => {
EventHandler.trigger(this._element, EVENT_HOVER, {
value: target.value
})
this._createTooltip(target.parentElement, target.value)
})
EventHandler.on(this._element, EVENT_FOCUSOUT, SELECTOR_RATING_ITEM_INPUT, () => {
EventHandler.trigger(this._element, EVENT_HOVER, {
value: null
})
if (this._tooltip) {
this._tooltip.hide()
}
})
}
_createTooltip(selector, value) {
if (this._config.tooltips === false) {
return
}
if (this._tooltip) {
this._tooltip.hide()
}
let tooltipTitle
if (typeof this._config.tooltips === 'boolean') {
tooltipTitle = value
}
if (typeof this._config.tooltips === 'object') {
tooltipTitle = this._config.tooltips[value]
}
if (Array.isArray(this._config.tooltips)) {
tooltipTitle = this._config.tooltips[value - 1]
}
this._tooltip = new Tooltip(selector, {
title: tooltipTitle
})
}
_configAfterMerge(config) {
if (typeof config.tooltips === 'string') {
config.tooltips = config.tooltips.split(',')
}
return config
}
_resetLabels() {
const labels = SelectorEngine.find(SELECTOR_RATING_ITEM_LABEL, this._element)
for (const label of labels) {
label.classList.remove(CLASS_NAME_ACTIVE)
}
}
_createRating() {
this._element.classList.add(CLASS_NAME_RATING)
if (this._config.size) {
this._element.classList.add(`rating-${this._config.size}`)
}
if (this._config.disabled) {
this._element.classList.add(CLASS_NAME_DISABLED)
}
if (this._config.readOnly) {
this._element.classList.add(CLASS_NAME_READONLY)
}
this._element.setAttribute('role', 'radiogroup')
Array.from({ length: this._config.itemCount }, (_, index) => this._createRatingItem(index))
}
_createRatingItem(index) {
const ratingItemElement = document.createElement('div')
ratingItemElement.classList.add(CLASS_NAME_RATING_ITEM)
const numberOfRadios = 1 / this._config.precision
// eslint-disable-next-line array-callback-return
Array.from({ length: numberOfRadios }, (_, _index) => {
const ratingItemId = getUID(`${this.constructor.NAME}${index}`).toString()
const isNotLastItem = _index + 1 < numberOfRadios
const value = numberOfRadios === 1 ? index + 1 : ((_index + 1) * (Number(this._config.precision))) + index
// Create label
const ratingItemLabelElement = document.createElement('label')
ratingItemLabelElement.classList.add(CLASS_NAME_RATING_ITEM_LABEL)
ratingItemLabelElement.setAttribute('for', ratingItemId)
// eslint-disable-next-line eqeqeq
if (this._config.highlightOnlySelected && this._currentValue == value) {
ratingItemLabelElement.classList.add(CLASS_NAME_ACTIVE)
}
if (!this._config.highlightOnlySelected && this._currentValue >= value) {
ratingItemLabelElement.classList.add(CLASS_NAME_ACTIVE)
}
if (isNotLastItem) {
ratingItemLabelElement.style.zIndex = (1 / this._config.precision) - _index
ratingItemLabelElement.style.position = 'absolute'
ratingItemLabelElement.style.width = `${this._config.precision * (_index + 1) * 100}%`
ratingItemLabelElement.style.overflow = 'hidden'
ratingItemLabelElement.style.opacity = 0
}
if (this._config.icon) {
const ratingItemIconElement = document.createElement('div')
ratingItemIconElement.classList.add(CLASS_NAME_RATING_ITEM_CUSTOM_ICON)
ratingItemIconElement.innerHTML = this._sanitizeIcon(typeof this._config.icon === 'object' ? this._config.icon[index + 1] : this._config.icon)
ratingItemLabelElement.append(ratingItemIconElement)
} else {
const ratingItemIconElement = document.createElement('div')
ratingItemIconElement.classList.add(CLASS_NAME_RATING_ITEM_ICON)
ratingItemLabelElement.append(ratingItemIconElement)
}
if (this._config.icon && this._config.activeIcon) {
const ratingItemIconActiveElement = document.createElement('div')
ratingItemIconActiveElement.classList.add(CLASS_NAME_RATING_ITEM_CUSTOM_ICON_ACTIVE)
ratingItemIconActiveElement.innerHTML = this._sanitizeIcon(typeof this._config.activeIcon === 'object' ? this._config.activeIcon[index + 1] : this._config.activeIcon)
ratingItemLabelElement.append(ratingItemIconActiveElement)
}
// Create input
const ratingItemInputElement = document.createElement('input')
ratingItemInputElement.classList.add(CLASS_NAME_RATING_ITEM_INPUT)
ratingItemInputElement.id = ratingItemId
ratingItemInputElement.type = 'radio'
ratingItemInputElement.value = value
ratingItemInputElement.name = this._name
if (this._config.disabled || this._config.readOnly) {
ratingItemInputElement.setAttribute('disabled', true)
}
if (this._currentValue === value) {
ratingItemInputElement.checked = true
}
// Append elements
if (this._config.precision === 1) {
ratingItemElement.append(ratingItemLabelElement)
ratingItemElement.append(ratingItemInputElement)
} else {
const wrapper = document.createElement('div')
wrapper.append(ratingItemLabelElement)
wrapper.append(ratingItemInputElement)
ratingItemElement.append(wrapper)
}
})
this._element.append(ratingItemElement)
}
_sanitizeIcon(icon) {
return this._config.sanitize ? sanitizeHtml(icon, this._config.allowList, this._config.sanitizeFn) : icon
}
_getConfig(config) {
const dataAttributes = Manipulator.getDataAttributes(this._element)
for (const dataAttribute of Object.keys(dataAttributes)) {
if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {
delete dataAttributes[dataAttribute]
}
}
config = {
...dataAttributes,
...(typeof config === 'object' && config ? config : {})
}
config = this._mergeConfigObj(config, this._element)
config = this._configAfterMerge(config)
this._typeCheckConfig(config)
return config
}
// Static
static ratingInterface(element, config) {
const data = Rating.getOrCreateInstance(element, config)
if (typeof config === 'string') {
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
}
}
static jQueryInterface(config) {
return this.each(function () {
const data = Rating.getOrCreateInstance(this, config)
if (typeof config !== 'string') {
return
}
if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {
throw new TypeError(`No method named "${config}"`)
}
data[config](this)
})
}
}
/**
* Data API implementation
*/
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
const ratings = SelectorEngine.find(SELECTOR_DATA_TOGGLE)
for (let i = 0, len = ratings.length; i < len; i++) {
Rating.ratingInterface(ratings[i])
}
})
/**
* jQuery
*/
defineJQueryPlugin(Rating)
export default Rating