@mezereon/ui-components-vue
Version:
UI components for Mezereon - Vue
695 lines (616 loc) • 21.2 kB
JavaScript
/* eslint-disable */
module.exports = (element, options = {}) => {
const isNumber = (n) => {
// check for NaN explicitly
// because with NaN, the second exp. evaluates to true
return !isNaN(n) && +n + '' === n + ''
}
const setMinMaxProps = (min = 0, max = 0) => {
return { min, max }
}
const iterateMinMaxProps = (fn) => {
;[MIN, MAX].forEach(fn)
}
const getSetProps = (condition, expression, fn) => {
if (condition) {
return expression
} else {
fn()
}
}
const setNodeAttribute = (node, attribute, value = '') => {
node.setAttribute(attribute, value)
}
const removeNodeAttribute = (node, attribute) => {
node.removeAttribute(attribute)
}
const addNodeEventListener = (node, event, fn, isPointerEvent = true) => {
// with options for pointer events
node.addEventListener(
event,
fn,
isPointerEvent ? { passive: false, capture: true } : {}
)
}
const fallbackToDefault = (property, defaultValue) => {
options[property] = {}.hasOwnProperty.call(options, property)
? options[property]
: defaultValue
}
const ifVerticalElse = (vertical, horizontal) => {
return options.orientation === VERTICAL ? vertical : horizontal
}
const currentIndex = (i) => {
return i === 1 ? index.max : index.min
}
// Set min and max values to 1 (arbitrarily) if any of the min or max values are "invalid"
// Setting both values 1 will disable the slider
// Called when,
// -> the element is initially set
// -> min or max properties are modified
const safeMinMaxValues = () => {
let error = false
if (!isNumber(options.min) || !isNumber(options.max)) {
error = true
}
options.min = error ? 1 : +options.min
options.max = error ? 1 : +options.max
}
// Reframe the thumbsDisabled value if "invalid"
// Called when,
// -> the element is initially set
// -> thumbsDisabled property is modified
const safeThumbsDisabledValues = () => {
if (options.thumbsDisabled instanceof Array) {
if (options.thumbsDisabled.length === 1) {
options.thumbsDisabled.push(false)
}
if (
options.thumbsDisabled.length !== 1 &&
options.thumbsDisabled.length !== 2
) {
options.thumbsDisabled = [false, false]
}
} else {
options.thumbsDisabled = [options.thumbsDisabled, options.thumbsDisabled]
}
// Boolean Values
options.thumbsDisabled[0] = !!options.thumbsDisabled[0]
options.thumbsDisabled[1] = !!options.thumbsDisabled[1]
}
// Called when,
// -> the element is initially set
// -> min, max, step or value properties are modified
// -> thumbs are dragged
// -> element is clicked upon
// -> an arrow key is pressed
const setValue = (newValue, forceSet = false, callback = true) => {
// Current value as set in the input elements
// which could change while changing min, max and step values
const currentValue = setMinMaxProps(
input[index.min].value,
input[index.max].value
)
// var value is synced with the values set in the input elements if no newValue is passed
newValue = newValue || currentValue
input[index.min].value = newValue.min
input[index.max].value =
thumbDrag || forceSet ? newValue.max : newValue.min + rangeWidth
syncValues()
// Check if the thumbs cross each other
if (value.min > value.max) {
// Switch thumb indexes
index.min = +!index.min
index.max = +!index.max
// Switch thumb attributes
removeNodeAttribute(thumb[index.min], DATA_UPPER)
removeNodeAttribute(thumb[index.max], DATA_LOWER)
setNodeAttribute(thumb[index.min], DATA_LOWER)
setNodeAttribute(thumb[index.max], DATA_UPPER)
// Switch thumb drag labels
if (thumbDrag) {
thumbDrag = thumbDrag === MIN ? MAX : MIN
}
syncValues()
}
const tooltipLowerText = element.querySelector(
`.mz-range-slider__thumb[${DATA_LOWER}] .mz-range-slider__tooltip`
)
const tooltipUpperText = element.querySelector(
`.mz-range-slider__thumb[${DATA_UPPER}] .mz-range-slider__tooltip`
)
tooltipLowerText.innerHTML = value.min
tooltipUpperText.innerHTML = value.max
sliderValue = forceSet ? value : newValue
let valueSet = false
if (currentValue.min !== input[index.min].value || forceSet) {
valueSet = true
}
if (currentValue.max !== input[index.max].value || forceSet) {
valueSet = true
}
// Update the positions, dimensions and aria attributes everytime a value is set
// and call the onInput function from options (if set)
if (valueSet) {
if (callback && options.onInput) {
options.onInput([value.min, value.max])
}
syncThumbDimensions()
updateThumbs()
updateRange()
updateAriaValueAttributes()
}
}
// Sync var value with the input elements
const syncValues = () => {
iterateMinMaxProps((_) => {
value[_] = +input[index[_]].value
})
}
// Called when,
// -> setValue is called and a value is set
// -> window is resized
const updateThumbs = () => {
iterateMinMaxProps((_) => {
thumb[index[_]].style[ifVerticalElse('top', 'left')] = `calc(${
((value[_] - options.min) / maxRangeWidth) * 100
}%`
})
}
// Called when,
// -> setValue is called and a value is set
// -> window is resized
const updateRange = () => {
const deltaOffset =
((0.5 - (value.min - options.min) / maxRangeWidth) *
ifVerticalElse(thumbHeight, thumbWidth).min) /
element[`client${ifVerticalElse('Height', 'Width')}`]
const deltaDimension =
((0.5 - (value.max - options.min) / maxRangeWidth) *
ifVerticalElse(thumbHeight, thumbWidth).max) /
element[`client${ifVerticalElse('Height', 'Width')}`]
range.style[ifVerticalElse('top', 'left')] = `${
((value.min - options.min) / maxRangeWidth + deltaOffset) * 100
}%`
range.style[ifVerticalElse('height', 'width')] = `${
((value.max - options.min) / maxRangeWidth -
(value.min - options.min) / maxRangeWidth -
deltaOffset +
deltaDimension) *
100
}%`
}
// Called when,
// -> the element is initially set
// -> min, max or value properties are modified
// -> range is dragged
// -> thumbs are disabled / enabled
const updateRangeLimits = () => {
iterateMinMaxProps((_, i) => {
rangeLimits[_] = options.thumbsDisabled[i] ? value[_] : options[_]
})
}
// Called when,
// -> thumbs are initially set
// -> thumbs are disabled / enabled
const updateTabIndexes = () => {
iterateMinMaxProps((_, i) => {
if (!options.disabled && !options.thumbsDisabled[i]) {
setNodeAttribute(thumb[currentIndex(i)], TABINDEX, 0)
} else {
removeNodeAttribute(thumb[currentIndex(i)], TABINDEX)
}
})
}
// Called when,
// -> setValue is called and a value is set
const updateAriaValueAttributes = () => {
iterateMinMaxProps((_) => {
setNodeAttribute(thumb[index[_]], 'aria-valuemin', options.min)
setNodeAttribute(thumb[index[_]], 'aria-valuemax', options.max)
setNodeAttribute(thumb[index[_]], 'aria-valuenow', value[_])
setNodeAttribute(thumb[index[_]], 'aria-valuetext', value[_])
})
}
// Called when,
// -> disabled property is modified
const updateDisabledState = () => {
if (options.disabled) {
setNodeAttribute(element, DATA_DISABLED)
} else {
removeNodeAttribute(element, DATA_DISABLED)
}
}
// Called when,
// -> thumbsDisabled property is modified
const updateThumbsDisabledState = () => {
options.thumbsDisabled.forEach((d, i) => {
const currIndex = currentIndex(i)
if (d) {
setNodeAttribute(thumb[currIndex], DATA_DISABLED)
setNodeAttribute(thumb[currIndex], 'aria-disabled', true)
} else {
removeNodeAttribute(thumb[currIndex], DATA_DISABLED)
setNodeAttribute(thumb[currIndex], 'aria-disabled', false)
}
})
}
// Called when,
// -> min or max values are modified
const updateLimits = (limit, m = false) => {
options[limit] = m
safeMinMaxValues()
iterateMinMaxProps((_) => {
input[0][_] = options[_]
input[1][_] = options[_]
})
maxRangeWidth = options.max - options.min
setValue('', true)
updateRangeLimits()
}
// Called when,
// -> the element is initially set
// -> orientation property is modified
const updateOrientation = () => {
if (options.orientation === VERTICAL) {
setNodeAttribute(element, DATA_VERTICAL)
} else {
removeNodeAttribute(element, DATA_VERTICAL)
}
range.style[ifVerticalElse('left', 'top')] = ''
range.style[ifVerticalElse('width', 'height')] = ''
thumb[0].style[ifVerticalElse('left', 'top')] = ''
thumb[1].style[ifVerticalElse('left', 'top')] = ''
}
// thumb width & height values are to be synced with the CSS values for correct calculation of
// thumb position and range width & position
// Called when,
// -> setValue is called and a value is set (called before updateThumbs() and updateRange())
// -> thumb / range drag is initiated
// -> window is resized
const syncThumbDimensions = () => {
iterateMinMaxProps((_) => {
thumbWidth[_] = float(style(thumb[index[_]]).width)
thumbHeight[_] = float(style(thumb[index[_]]).height)
})
}
// thumb position calculation depending upon the pointer position
const currentPosition = (e, node) => {
const currPos =
((node[`offset${ifVerticalElse('Top', 'Left')}`] +
(e[`client${ifVerticalElse('Y', 'X')}`] -
node.getBoundingClientRect()[ifVerticalElse('top', 'left')]) -
(thumbDrag
? (0.5 - (value[thumbDrag] - options.min) / maxRangeWidth) *
ifVerticalElse(thumbHeight, thumbWidth)[thumbDrag]
: 0)) /
element[`client${ifVerticalElse('Height', 'Width')}`]) *
maxRangeWidth +
options.min
if (currPos < options.min) {
return options.min
}
if (currPos > options.max) {
return options.max
}
return currPos
}
const doesntHaveClassName = (e, className) => {
return !e.target.classList.contains(className)
}
const elementFocused = (e) => {
let setFocus = false
if (
!options.disabled &&
((doesntHaveClassName(e, 'mz-range-slider__thumb') &&
doesntHaveClassName(e, 'mz-range-slider__range')) ||
(options.rangeSlideDisabled &&
doesntHaveClassName(e, 'mz-range-slider__thumb')))
) {
setFocus = true
}
// No action if both thumbs are disabled
if (setFocus && options.thumbsDisabled[0] && options.thumbsDisabled[1]) {
setFocus = false
}
if (setFocus) {
const currPos = currentPosition(e, range)
const deltaMin = abs(value.min - currPos)
const deltaMax = abs(value.max - currPos)
if (options.thumbsDisabled[0]) {
if (currPos >= value.min) {
setValue(setMinMaxProps(value.min, currPos), true)
initiateThumbDrag(e, index.max, thumb[index.max])
}
} else if (options.thumbsDisabled[1]) {
if (currPos <= value.max) {
setValue(setMinMaxProps(currPos, value.max), true)
initiateThumbDrag(e, index.min, thumb[index.min])
}
} else {
let nearestThumbIndex = index.max
if (deltaMin === deltaMax) {
setValue(setMinMaxProps(value.min, currPos), true)
} else {
setValue(
setMinMaxProps(
deltaMin < deltaMax ? currPos : value.min,
deltaMax < deltaMin ? currPos : value.max
),
true
)
nearestThumbIndex = deltaMin < deltaMax ? index.min : index.max
}
initiateThumbDrag(e, nearestThumbIndex, thumb[nearestThumbIndex])
}
}
}
const initiateDrag = (e, node) => {
syncThumbDimensions()
setNodeAttribute(node, DATA_ACTIVE)
startPos = currentPosition(e, node)
isDragging = true
}
const initiateThumbDrag = (e, i, node) => {
if (!options.disabled && !options.thumbsDisabled[currentIndex(i)]) {
initiateDrag(e, node)
thumbDrag = index.min === i ? MIN : MAX
if (options.onThumbDragStart) {
options.onThumbDragStart()
}
}
}
const initiateRangeDrag = (e) => {
if (!options.disabled && !options.rangeSlideDisabled) {
initiateDrag(e, range)
rangeWidth = value.max - value.min
thumbDrag = false
if (options.onRangeDragStart) {
options.onRangeDragStart()
}
}
}
const drag = (e) => {
if (isDragging) {
const lastPos = currentPosition(e, range)
const delta = lastPos - startPos
let min = value.min
let max = value.max
const lower = thumbDrag ? rangeLimits.min : options.min
const upper = thumbDrag ? rangeLimits.max : options.max
if (!thumbDrag || thumbDrag === MIN) {
min = thumbDrag ? lastPos : sliderValue.min + delta
}
if (!thumbDrag || thumbDrag === MAX) {
max = thumbDrag ? lastPos : sliderValue.max + delta
}
if (min >= lower && min <= upper && max >= lower && max <= upper) {
setValue({ min, max })
startPos = lastPos
} else {
// When min thumb reaches upper limit
if (min > upper && thumbDrag) {
setValue(setMinMaxProps(upper, upper))
startPos = lastPos
}
// When max thumb reaches lower limit
if (max < lower && thumbDrag) {
setValue(setMinMaxProps(lower, lower))
startPos = lastPos
}
// When range / min thumb reaches lower limit
if (min < lower) {
if (!thumbDrag) {
setValue(setMinMaxProps(lower, value.max - value.min + lower))
} else {
setValue(setMinMaxProps(lower, value.max))
}
startPos = lastPos
}
// When range / max thumb reaches upper limit
if (max > upper) {
if (!thumbDrag) {
setValue(setMinMaxProps(value.min - value.max + upper, upper))
} else {
setValue(setMinMaxProps(value.min, upper))
}
startPos = lastPos
}
}
if (!thumbDrag) {
updateRangeLimits()
}
}
}
const actualStepValue = () => {
const step = float(input[0].step)
return input[0].step === ANY ? ANY : step === 0 || isNaN(step) ? 1 : step
}
// Step value (up or down) using arrow keys
const stepValue = (i, key) => {
const direction =
(key === 37 || key === 40 ? -1 : 1) * ifVerticalElse(-1, 1)
if (!options.disabled && !options.thumbsDisabled[currentIndex(i)]) {
let step = actualStepValue()
step = step === ANY ? 1 : step
let min = value.min + step * (index.min === i ? direction : 0)
let max = value.max + step * (index.max === i ? direction : 0)
// When min thumb reaches upper limit
if (min > rangeLimits.max) {
min = rangeLimits.max
}
// When max thumb reaches lower limit
if (max < rangeLimits.min) {
max = rangeLimits.min
}
setValue({ min, max }, true)
}
}
// Aliases
const abs = Math.abs
const float = parseFloat
const style = window.getComputedStyle
// Values
const MIN = 'min'
const MAX = 'max'
const ANY = 'any'
const VERTICAL = 'vertical'
const TABINDEX = 'tabindex'
// Data Attributes
const DATA_LOWER = 'data-lower'
const DATA_UPPER = 'data-upper'
const DATA_ACTIVE = 'data-active'
const DATA_VERTICAL = 'data-vertical'
const DATA_DISABLED = 'data-disabled'
// Actual value
const value = setMinMaxProps()
// Thumb indexes for min and max values
// (swapped when the thumbs cross each other)
const index = setMinMaxProps(0, 1)
// Thumb width & height for calculation of exact positions and sizes of horizontal thumbs and range
const thumbWidth = setMinMaxProps()
const thumbHeight = setMinMaxProps()
// Slidable range limits (when a thumb is dragged)
const rangeLimits = setMinMaxProps()
// Slider value depending on the user interaction
let sliderValue = setMinMaxProps()
// For dragging thumbs and range
let maxRangeWidth = 0
let rangeWidth = 0
let isDragging = false
let thumbDrag = false
let startPos = 0
// Set options to default values if not set
fallbackToDefault('rangeSlideDisabled', false)
fallbackToDefault('thumbsDisabled', [false, false])
fallbackToDefault('orientation', 'horizontal')
fallbackToDefault('disabled', false)
fallbackToDefault('onThumbDragStart', false)
fallbackToDefault('onRangeDragStart', false)
fallbackToDefault('onThumbDragEnd', false)
fallbackToDefault('onRangeDragEnd', false)
fallbackToDefault('onInput', false)
fallbackToDefault('value', [25, 75])
fallbackToDefault('step', 1)
fallbackToDefault('min', 0)
fallbackToDefault('max', 100)
safeMinMaxValues()
safeThumbsDisabledValues()
// Fill wrapper element
const tooltip = (value) =>
`<div class="mz-range-slider__tooltip-top"><div class="mz-range-slider__tooltip-inner"><span class="mz-range-slider__tooltip">${value}</span></div></div>`
const tooltipLower = tooltip(options.value[0])
const tooltipUpper = tooltip(options.value[1])
element.innerHTML = `<input type="range" min="${options.min}" max="${options.max}" step="${options.step}" value="${options.value[0]}" disabled><input type="range" min="${options.min}" max="${options.max}" step="${options.step}" value="${options.value[1]}" disabled><div role="slider" class="mz-range-slider__thumb" ${DATA_LOWER}>${tooltipLower}</div><div role="slider" class="mz-range-slider__thumb" ${DATA_UPPER}>${tooltipUpper}</div><div class="mz-range-slider__range"></div>`
element.classList.add('mz-range-slider')
const range = element.querySelector('.mz-range-slider__range')
const input = element.querySelectorAll('input')
const thumb = element.querySelectorAll('.mz-range-slider__thumb')
// Set initial values
maxRangeWidth = options.max - options.min
setValue('', true, false)
updateRangeLimits()
updateDisabledState()
updateThumbsDisabledState()
updateTabIndexes()
updateOrientation()
// Add listeners to element
addNodeEventListener(element, 'pointerdown', (e) => {
elementFocused(e)
})
// Add listeners to thumbs and set [data-disabled] on disabled thumbs
Array.from(thumb).forEach((t, i) => {
addNodeEventListener(t, 'pointerdown', (e) => {
initiateThumbDrag(e, i, t)
})
addNodeEventListener(t, 'keydown', (e) => {
if (e.which >= 37 && e.which <= 40) {
e.preventDefault()
stepValue(i, e.which)
}
})
})
// Add listeners to range
addNodeEventListener(range, 'pointerdown', (e) => {
initiateRangeDrag(e)
})
// Add global listeners
addNodeEventListener(document, 'pointermove', (e) => {
drag(e)
})
addNodeEventListener(document, 'pointerup', () => {
if (isDragging) {
removeNodeAttribute(thumb[0], DATA_ACTIVE)
removeNodeAttribute(thumb[1], DATA_ACTIVE)
removeNodeAttribute(range, DATA_ACTIVE)
isDragging = false
if (thumbDrag) {
if (options.onThumbDragEnd) {
options.onThumbDragEnd()
}
} else {
if (options.onRangeDragEnd) {
options.onRangeDragEnd()
}
}
}
})
addNodeEventListener(window, 'resize', () => {
syncThumbDimensions()
updateThumbs()
updateRange()
})
return {
min: (m = false) => {
return getSetProps(!m && m !== 0, options.min, () => {
updateLimits(MIN, m)
})
},
max: (m = false) => {
return getSetProps(!m && m !== 0, options.max, () => {
updateLimits(MAX, m)
})
},
step: (s = false) => {
return getSetProps(!s, actualStepValue(), () => {
input[0].step = s
input[1].step = s
setValue('', true)
})
},
value: (v = false) => {
return getSetProps(!v, [value.min, value.max], () => {
setValue(setMinMaxProps(v[0], v[1]), true)
updateRangeLimits()
})
},
orientation: (o = false) => {
return getSetProps(!o, options.orientation, () => {
options.orientation = o
updateOrientation()
setValue('', true)
})
},
disabled: (d = true) => {
options.disabled = !!d
updateDisabledState()
},
thumbsDisabled: (t = [true, true]) => {
options.thumbsDisabled = t
safeThumbsDisabledValues()
updateRangeLimits()
updateTabIndexes()
updateThumbsDisabledState()
},
rangeSlideDisabled: (d = true) => {
options.rangeSlideDisabled = !!d
},
currentValueIndex: () => {
return thumbDrag ? (thumbDrag === MIN ? 0 : 1) : -1
},
setValue: (newValue, forceSet = false, callback = true) => {
const converted = setMinMaxProps(newValue[0], newValue[1])
return setValue(converted, forceSet, callback)
}
}
}