vanilla-rangeslider
Version:
Ion-RangeSlider without out jQuery
1,576 lines (1,299 loc) • 64.7 kB
JavaScript
const IonRangeSlider = function (element, initOptions= {}) {
let input = element;
let options = initOptions;
let calc_count = 0;
let update_tm = 0;
let old_from = 0;
let old_to = 0;
let old_min_interval = null;
let raf_id = null;
let dragging = false;
let force_redraw = false;
let no_diapason = false;
let has_tab_index = true;
let is_key = false;
let is_update = false;
let is_start = true;
let is_finish = false;
let is_active = false;
let is_resize = false;
let is_click = false;
let target = "base";
// cache for links to all DOM elements
const cache = {
win: window,
body: document.body,
input: input,
cont: null,
rs: null,
min: null,
max: null,
from: null,
to: null,
single: null,
bar: null,
line: null,
s_single: null,
s_from: null,
s_to: null,
shad_single: null,
shad_from: null,
shad_to: null,
edge: null,
grid: null,
grid_labels: []
};
// storage for measure variables
const coords = {
// left
x_gap: 0,
x_pointer: 0,
// width
w_rs: 0,
w_rs_old: 0,
w_handle: 0,
// percents
p_gap: 0,
p_gap_left: 0,
p_gap_right: 0,
p_step: 0,
p_pointer: 0,
p_handle: 0,
p_single_fake: 0,
p_single_real: 0,
p_from_fake: 0,
p_from_real: 0,
p_to_fake: 0,
p_to_real: 0,
p_bar_x: 0,
p_bar_w: 0,
// grid
grid_gap: 0,
big_num: 0,
big: [],
big_w: [],
big_p: [],
big_x: []
};
// storage for labels measure variables
const labels = {
// width
w_min: 0,
w_max: 0,
w_from: 0,
w_to: 0,
w_single: 0,
// percents
p_min: 0,
p_max: 0,
p_from_fake: 0,
p_from_left: 0,
p_to_fake: 0,
p_to_left: 0,
p_single_fake: 0,
p_single_left: 0
};
// default config
const config = {
skin: "flat",
type: "single",
min: 10,
max: 100,
from: null,
to: null,
step: 1,
min_interval: 0,
max_interval: 0,
drag_interval: false,
values: [],
p_values: [],
from_fixed: false,
from_min: null,
from_max: null,
from_shadow: false,
to_fixed: false,
to_min: null,
to_max: null,
to_shadow: false,
prettify_enabled: true,
prettify_separator: " ",
prettify: null,
force_edges: false,
keyboard: true,
grid: false,
grid_margin: true,
grid_num: 4,
grid_snap: false,
hide_min_max: false,
hide_from_to: false,
prefix: "",
postfix: "",
max_postfix: "",
decorate_both: true,
values_separator: " — ",
input_values_separator: ";",
disable: false,
block: false,
extra_classes: "",
scope: null,
onStart: null,
onChange: null,
onFinish: null,
onUpdate: null
};
// check if base element is input
if (input.nodeName !== "INPUT") {
console && console.warn && console.warn("Base element should be <input>!", input);
}
// config from data-attributes extends js config
const config_from_data = {
skin: input.dataset.skin,
type: input.dataset.type,
min: input.dataset.min,
max: input.dataset.max,
from: input.dataset.from,
to: input.dataset.to,
step: input.dataset.step,
min_interval: input.dataset.minInterval,
max_interval: input.dataset.maxInterval,
drag_interval: input.dataset.dragInterval,
values: input.dataset.values,
from_fixed: input.dataset.fromFixed,
from_min: input.dataset.fromMin,
from_max: input.dataset.fromMax,
from_shadow: input.dataset.fromShadow,
to_fixed: input.dataset.toFixed,
to_min: input.dataset.toMin,
to_max: input.dataset.toMax,
to_shadow: input.dataset.toShadow,
prettify_enabled: input.dataset.prettifyEnabled,
prettify_separator: input.dataset.prettifySeparator,
force_edges: input.dataset.forceEdges,
keyboard: input.dataset.keyboard,
grid: input.dataset.grid,
grid_margin: input.dataset.gridMargin,
grid_num: input.dataset.gridNum,
grid_snap: input.dataset.gridSnap,
hide_min_max: input.dataset.hideMinMax,
hide_from_to: input.dataset.hideFromTo,
prefix: input.dataset.prefix,
postfix: input.dataset.postfix,
max_postfix: input.dataset.maxPostfix,
decorate_both: input.dataset.decorateBoth,
values_separator: input.dataset.valuesSeparator,
input_values_separator: input.dataset.inputValuesSeparator,
disable: input.dataset.disable,
block: input.dataset.block,
extra_classes: input.dataset.extraClasses,
};
config_from_data.values = config_from_data.values && config_from_data.values.split(",");
for (let prop in config_from_data) {
if (config_from_data.hasOwnProperty(prop)) {
if (config_from_data[prop] === undefined || config_from_data[prop] === "") {
delete config_from_data[prop];
}
}
}
// input value extends default config
let val = input.value;
if (val !== undefined && val !== "") {
val = val.split(config_from_data.input_values_separator || options.input_values_separator || ";");
if (val[0] && val[0] == +val[0]) {
val[0] = +val[0];
}
if (val[1] && val[1] == +val[1]) {
val[1] = +val[1];
}
if (options && options.values && options.values.length) {
config.from = val[0] && options.values.indexOf(val[0]);
config.to = val[1] && options.values.indexOf(val[1]);
} else {
config.from = val[0] && +val[0];
config.to = val[1] && +val[1];
}
}
// js config extends default config
Object.assign(config, options);
// data config extends config
Object.assign(config, config_from_data);
options = config;
let update_check = {};
// default result object, returned to callbacks
const result = {
input: cache.input,
slider: null,
min: options.min,
max: options.max,
from: options.from,
from_percent: 0,
from_value: null,
to: options.to,
to_percent: 0,
to_value: null
};
// HTML Templates
const base_html =
'<span class="irs">' +
'<span class="irs-line" tabindex="0"></span>' +
'<span class="irs-min">0</span><span class="irs-max">1</span>' +
'<span class="irs-from">0</span><span class="irs-to">0</span><span class="irs-single">0</span>' +
'</span>' +
'<span class="irs-grid">1</span>';
const single_html =
'<span class="irs-bar irs-bar--single"></span>' +
'<span class="irs-shadow shadow-single"></span>' +
'<span class="irs-handle single"><i></i><i></i><i></i></span>';
const double_html =
'<span class="irs-bar"></span>' +
'<span class="irs-shadow shadow-from"></span>' +
'<span class="irs-shadow shadow-to"></span>' +
'<span class="irs-handle from"><i></i><i></i><i></i></span>' +
'<span class="irs-handle to"><i></i><i></i><i></i></span>';
const disable_html =
'<span class="irs-disable-mask"></span>';
const init = function (is_update) {
no_diapason = false;
coords.p_step = convertToPercent(options.step, true);
target = "base";
toggleInput();
append();
setMinMax();
if (is_update) {
force_redraw = true;
calc(true);
// callbacks called
callOnUpdate();
} else {
force_redraw = true;
calc(true);
// callbacks called
callOnStart();
}
updateScene();
};
/**
* Appends slider template to a DOM
*/
const append = function () {
const container_html = '<span class="irs irs--' + options.skin + ' ' + options.extra_classes + '"></span>';
cache.input.insertAdjacentHTML('beforebegin', container_html);
cache.input.setAttribute("readonly", "true");
cache.cont = cache.input.previousElementSibling;
result.slider = cache.cont;
cache.cont.innerHTML = base_html;
cache.rs = cache.cont.querySelector(".irs");
cache.min = cache.cont.querySelector(".irs-min");
cache.max = cache.cont.querySelector(".irs-max");
cache.from = cache.cont.querySelector(".irs-from");
cache.to = cache.cont.querySelector(".irs-to");
cache.single = cache.cont.querySelector(".irs-single");
cache.line = cache.cont.querySelector(".irs-line");
cache.grid = cache.cont.querySelector(".irs-grid");
if (options.type === "single") {
cache.cont.insertAdjacentHTML('beforeend', single_html);
cache.bar = cache.cont.querySelector(".irs-bar");
cache.edge = cache.cont.querySelector(".irs-bar--single");
cache.s_single = cache.cont.querySelector(".single");
cache.from.style.visibility = "hidden";
cache.to.style.visibility = "hidden";
cache.shad_single = cache.cont.querySelector(".shadow-single");
} else {
cache.cont.insertAdjacentHTML('beforeend', double_html);
cache.bar = cache.cont.querySelector(".irs-bar");
cache.s_from = cache.cont.querySelector(".from");
cache.s_to = cache.cont.querySelector(".to");
cache.shad_from = cache.cont.querySelector(".shadow-from");
cache.shad_to = cache.cont.querySelector(".shadow-to");
setTopHandler();
}
if (options.hide_from_to) {
cache.from.style.display = "none";
cache.to.style.display = "none";
cache.single.style.display = "none";
}
appendGrid();
if (options.disable) {
appendDisableMask();
cache.input.disabled = true;
} else {
cache.input.disabled = false;
removeDisableMask();
bindEvents();
}
// block only if not disabled
if (!options.disable) {
if (options.block) {
appendDisableMask();
} else {
removeDisableMask();
}
}
if (options.drag_interval) {
cache.bar.style.cursor = "ew-resize";
}
};
/**
* Determine which handler has a priority (works only for double slider type)
*/
const setTopHandler = function () {
const min = options.min,
max = options.max,
from = options.from,
to = options.to;
if (from > min && to === max) {
cache.s_from.classList.add("type_last");
} else if (to < max) {
cache.s_to.classList.add("type_last");
}
};
/**
* Determine which handles was clicked last and which handler should have hover effect
*
* @param target {String}
*/
const changeLevel = function (target) {
switch (target) {
case "single":
coords.p_gap = toFixed(coords.p_pointer - coords.p_single_fake);
cache.s_single.classList.add("state_hover");
break;
case "from":
coords.p_gap = toFixed(coords.p_pointer - coords.p_from_fake);
cache.s_from.classList.add("state_hover", "type_last");
cache.s_to.classList.remove("type_last");
break;
case "to":
coords.p_gap = toFixed(coords.p_pointer - coords.p_to_fake);
cache.s_to.classList.add("state_hover", "type_last");
cache.s_from.classList.remove("type_last");
break;
case "both":
coords.p_gap_left = toFixed(coords.p_pointer - coords.p_from_fake);
coords.p_gap_right = toFixed(coords.p_to_fake - coords.p_pointer);
cache.s_to.classList.remove("type_last");
cache.s_from.classList.remove("type_last");
break;
}
};
/**
* Then slider is disabled -> append extra layer with opacity
*/
const appendDisableMask = function () {
cache.cont.insertAdjacentHTML('beforeend', disable_html);
cache.cont.classList.add("irs-disabled");
};
/**
* Then slider is not disabled -> remove disable mask
*/
const removeDisableMask = function () {
cache.cont.classList.remove(".irs-disable-mask");
cache.cont.classList.remove("irs-disabled");
};
/**
* Remove slider instance and unbind all events
*/
const remove = function () {
cache.cont.remove();
cache.cont = null;
cache.win.removeEventListener("keydown", key.bind(this, 'keyboard'));
cache.body.removeEventListener("touchmove", pointerMove.bind(this));
cache.body.removeEventListener("mousemove", pointerMove.bind(this));
cache.win.removeEventListener("touchend", pointerUp.bind(this));
cache.win.removeEventListener("mouseup", pointerUp.bind(this));
cache.grid_labels = [];
coords.big = [];
coords.big_w = [];
coords.big_p = [];
coords.big_x = [];
cancelAnimationFrame(raf_id);
};
/**
* bind all slider events
*/
const bindEvents = function () {
if (no_diapason) {
return;
}
cache.body.addEventListener('touchmove', pointerMove.bind(this));
cache.body.addEventListener('mousemove', pointerMove.bind(this));
cache.win.addEventListener('touchend', pointerUp.bind(this));
cache.win.addEventListener('mouseup', pointerUp.bind(this));
cache.line.addEventListener('touchstart', pointerClick.bind(this, 'click'), {passive: true});
cache.line.addEventListener('mousedown', pointerClick.bind(this, 'click'));
cache.line.addEventListener('focus', pointerFocus.bind(this));
if (options.drag_interval && options.type === "double") {
cache.bar.addEventListener('touchstart', pointerDown.bind(this, 'both'), {passive: true});
cache.bar.addEventListener('mousedown', pointerDown.bind(this, 'both'));
} else {
cache.bar.addEventListener('touchstart', pointerClick.bind(this, 'click'), {passive: true});
cache.bar.addEventListener('mousedown', pointerClick.bind(this, 'click'));
}
if (options.type === "single") {
cache.single.addEventListener('touchstart', pointerDown.bind(this, 'single'), {passive: true});
cache.s_single.addEventListener('touchstart', pointerDown.bind(this, 'single'), {passive: true});
cache.shad_single.addEventListener('touchstart', pointerClick.bind(this, 'click'), {passive: true});
cache.single.addEventListener('mousedown', pointerDown.bind(this, 'single'));
cache.s_single.addEventListener('mousedown', pointerDown.bind(this, 'single'));
cache.edge.addEventListener('mousedown', pointerClick.bind(this, 'click'));
cache.shad_single.addEventListener('touchstart', pointerClick.bind(this, 'click'), {passive: true});
} else {
cache.single.addEventListener('touchstart', pointerDown.bind(this, null), {passive: true});
cache.single.addEventListener('mousedown', pointerDown.bind(this, null));
cache.from.addEventListener('touchstart', pointerDown.bind(this, 'from'), {passive: true});
cache.s_from.addEventListener('touchstart', pointerDown.bind(this, 'from'), {passive: true});
cache.to.addEventListener('touchstart', pointerDown.bind(this, 'to'), {passive: true});
cache.s_to.addEventListener('touchstart', pointerDown.bind(this, 'to'), {passive: true});
cache.shad_from.addEventListener('touchstart', pointerClick.bind(this, 'click'), {passive: true});
cache.shad_to.addEventListener('touchstart', pointerClick.bind(this, 'click'), {passive: true});
cache.from.addEventListener('mousedown', pointerDown.bind(this, 'from'));
cache.s_from.addEventListener('mousedown', pointerDown.bind(this, 'from'));
cache.to.addEventListener('mousedown', pointerDown.bind(this, 'to'));
cache.s_to.addEventListener('mousedown', pointerDown.bind(this, 'to'));
cache.shad_from.addEventListener('mousedown', pointerClick.bind(this, 'click'));
cache.shad_to.addEventListener('mousedown', pointerClick.bind(this, 'click'));
}
if (options.keyboard) {
cache.line.addEventListener('keydown', key.bind(this, 'keyboard'));
}
};
/**
* Focus with tabIndex
*
* @param e {Object} event object
*/
const pointerFocus = function (e) {
if (!target) {
let x, $handle;
if (options.type === "single") {
$handle = cache.single;
} else {
$handle = cache.from;
}
x = $handle.getBoundingClientRect().left;
x += ($handle.getBoundingClientRect().width / 2) - 1;
pointerClick("single", {preventDefault: function () {}, pageX: x});
} else {
cache.line.focus();
}
};
/**
* Mousemove or touchmove (only for handlers)
*
* @param e {Object} event object
*/
const pointerMove = function (e) {
if (!dragging) {
return;
}
const x = e.pageX || e.originalEvent.touches && e.originalEvent.touches[0].pageX; // TODO
coords.x_pointer = x - coords.x_gap;
calc();
};
/**
* Mouseup or touchend
* only for handlers
*
* @param e {Object} event object
*/
const pointerUp = function (e) {
if (is_active) {
is_active = false;
} else {
return;
}
const hoverState = cache.cont.querySelector(".state_hover");
if (hoverState) {
hoverState.classList.remove("state_hover");
}
force_redraw = true;
updateScene();
restoreOriginalMinInterval();
// callbacks call
if (cache.cont.contains(e.target) || dragging) {
callOnFinish();
}
dragging = false;
};
/**
* Mousedown or touchstart
* only for handlers
*
* @param destination {String|null}
* @param e {Object} event object
*/
const pointerDown = function (destination, e) {
e.preventDefault();
const x = e.pageX || e.originalEvent.touches && e.originalEvent.touches[0].pageX; // TODO
if (e.button === 2) {
return;
}
if (destination === "both") {
setTempMinInterval();
}
if (!destination) {
destination = target || "from";
}
target = destination;
is_active = true;
dragging = true;
coords.x_gap = cache.rs.getBoundingClientRect().left;
coords.x_pointer = x - coords.x_gap;
calcPointerPercent();
changeLevel(destination);
cache.line.dispatchEvent(new Event("focus"));
updateScene();
};
/**
* Mousedown or touchstart
* for other slider elements, like diapason line
*
* @param destination {String}
* @param e {Object} event object
*/
const pointerClick = function (destination, e) {
e.preventDefault();
const x = e.pageX || e.originalEvent.touches && e.originalEvent.touches[0].pageX; // TODO
if (e.button === 2) {
return;
}
target = destination;
is_click = true;
coords.x_gap = cache.rs.getBoundingClientRect().left;
coords.x_pointer = +(x - coords.x_gap).toFixed();
force_redraw = true;
calc();
cache.line.dispatchEvent(new Event("focus"));
};
/**
* Keyboard controls for focused slider
*
* @param destination {String}
* @param e {Object} event object
* @returns {boolean|undefined}
*/
const key = function (destination, e) {
if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
return;
}
switch (e.which) {
case 83: // W
case 65: // A
case 40: // DOWN
case 37: // LEFT
e.preventDefault();
moveByKey(false);
break;
case 87: // S
case 68: // D
case 38: // UP
case 39: // RIGHT
e.preventDefault();
moveByKey(true);
break;
}
};
/**
* Move by key
*
* @param right {boolean} direction to move
*/
const moveByKey = function (right) {
let p = coords.p_pointer;
const p_step = options.step / ((options.max - options.min) / 100);
right ? p += p_step : p -= p_step;
coords.x_pointer = toFixed(coords.w_rs / 100 * p);
is_key = true;
calc();
};
/**
* Set visibility and content
* of Min and Max labels
*/
const setMinMax = function () {
if (!options) {
return;
}
if (options.hide_min_max) {
cache.min.style.display = "none";
cache.max.style.display = "none";
return;
}
if (options.values.length) {
cache.min.innerHTML = decorate(options.p_values[options.min]);
cache.max.innerHTML = decorate(options.p_values[options.max]);
} else {
const min_pretty = _prettify(options.min);
const max_pretty = _prettify(options.max);
result.min_pretty = min_pretty;
result.max_pretty = max_pretty;
cache.min.innerHTML = decorate(min_pretty, options.min);
cache.max.innerHTML = decorate(max_pretty, options.max);
}
labels.w_min = cache.min.offsetWidth;
labels.w_max = cache.max.offsetWidth;
};
/**
* Then dragging interval, prevent interval collapsing
* using min_interval option
*/
const setTempMinInterval = function () {
const interval = result.to - result.from;
if (old_min_interval === null) {
old_min_interval = options.min_interval;
}
options.min_interval = interval;
};
const restoreOriginalMinInterval = function () {
if (old_min_interval !== null) {
options.min_interval = old_min_interval;
old_min_interval = null;
}
};
// Calculations
/**
* All calculations and measures start here
*
* @param update {boolean=}
*/
const calc = function (update) {
if (!options) {
return;
}
calc_count++;
if (calc_count === 10 || update) {
calc_count = 0;
coords.w_rs = cache.rs.offsetWidth;
calcHandlePercent();
}
if (!coords.w_rs) {
return;
}
calcPointerPercent();
let handle_x = getHandleX();
if (target === "both") {
coords.p_gap = 0;
handle_x = getHandleX();
}
if (target === "click") {
coords.p_gap = coords.p_handle / 2;
handle_x = getHandleX();
if (options.drag_interval) {
target = "both_one";
} else {
target = chooseHandle(handle_x);
}
}
switch (target) {
case "base":
const w = (options.max - options.min) / 100,
f = (result.from - options.min) / w,
t = (result.to - options.min) / w;
coords.p_single_real = toFixed(f);
coords.p_from_real = toFixed(f);
coords.p_to_real = toFixed(t);
coords.p_single_real = checkDiapason(coords.p_single_real, options.from_min, options.from_max);
coords.p_from_real = checkDiapason(coords.p_from_real, options.from_min, options.from_max);
coords.p_to_real = checkDiapason(coords.p_to_real, options.to_min, options.to_max);
coords.p_single_fake = convertToFakePercent(coords.p_single_real);
coords.p_from_fake = convertToFakePercent(coords.p_from_real);
coords.p_to_fake = convertToFakePercent(coords.p_to_real);
target = null;
break;
case "single":
if (options.from_fixed) {
break;
}
coords.p_single_real = convertToRealPercent(handle_x);
coords.p_single_real = calcWithStep(coords.p_single_real);
coords.p_single_real = checkDiapason(coords.p_single_real, options.from_min, options.from_max);
coords.p_single_fake = convertToFakePercent(coords.p_single_real);
break;
case "from":
if (options.from_fixed) {
break;
}
coords.p_from_real = convertToRealPercent(handle_x);
coords.p_from_real = calcWithStep(coords.p_from_real);
if (coords.p_from_real > coords.p_to_real) {
coords.p_from_real = coords.p_to_real;
}
coords.p_from_real = checkDiapason(coords.p_from_real, options.from_min, options.from_max);
coords.p_from_real = checkMinInterval(coords.p_from_real, coords.p_to_real, "from");
coords.p_from_real = checkMaxInterval(coords.p_from_real, coords.p_to_real, "from");
coords.p_from_fake = convertToFakePercent(coords.p_from_real);
break;
case "to":
if (options.to_fixed) {
break;
}
coords.p_to_real = convertToRealPercent(handle_x);
coords.p_to_real = calcWithStep(coords.p_to_real);
if (coords.p_to_real < coords.p_from_real) {
coords.p_to_real = coords.p_from_real;
}
coords.p_to_real = checkDiapason(coords.p_to_real, options.to_min, options.to_max);
coords.p_to_real = checkMinInterval(coords.p_to_real, coords.p_from_real, "to");
coords.p_to_real = checkMaxInterval(coords.p_to_real, coords.p_from_real, "to");
coords.p_to_fake = convertToFakePercent(coords.p_to_real);
break;
case "both":
if (options.from_fixed || options.to_fixed) {
break;
}
handle_x = toFixed(handle_x + (coords.p_handle * 0.001));
coords.p_from_real = convertToRealPercent(handle_x) - coords.p_gap_left;
coords.p_from_real = calcWithStep(coords.p_from_real);
coords.p_from_real = checkDiapason(coords.p_from_real, options.from_min, options.from_max);
coords.p_from_real = checkMinInterval(coords.p_from_real, coords.p_to_real, "from");
coords.p_from_fake = convertToFakePercent(coords.p_from_real);
coords.p_to_real = convertToRealPercent(handle_x) + coords.p_gap_right;
coords.p_to_real = calcWithStep(coords.p_to_real);
coords.p_to_real = checkDiapason(coords.p_to_real, options.to_min, options.to_max);
coords.p_to_real = checkMinInterval(coords.p_to_real, coords.p_from_real, "to");
coords.p_to_fake = convertToFakePercent(coords.p_to_real);
break;
case "both_one":
if (options.from_fixed || options.to_fixed) {
break;
}
const real_x = convertToRealPercent(handle_x),
from = result.from_percent,
to = result.to_percent,
full = to - from,
half = full / 2;
let new_from = real_x - half,
new_to = real_x + half;
if (new_from < 0) {
new_from = 0;
new_to = new_from + full;
}
if (new_to > 100) {
new_to = 100;
new_from = new_to - full;
}
coords.p_from_real = calcWithStep(new_from);
coords.p_from_real = checkDiapason(coords.p_from_real, options.from_min, options.from_max);
coords.p_from_fake = convertToFakePercent(coords.p_from_real);
coords.p_to_real = calcWithStep(new_to);
coords.p_to_real = checkDiapason(coords.p_to_real, options.to_min, options.to_max);
coords.p_to_fake = convertToFakePercent(coords.p_to_real);
break;
}
if (options.type === "single") {
coords.p_bar_x = (coords.p_handle / 2);
coords.p_bar_w = coords.p_single_fake;
result.from_percent = coords.p_single_real;
result.from = convertToValue(coords.p_single_real);
result.from_pretty = _prettify(result.from);
if (options.values.length) {
result.from_value = options.values[result.from];
}
} else {
coords.p_bar_x = toFixed(coords.p_from_fake + (coords.p_handle / 2));
coords.p_bar_w = toFixed(coords.p_to_fake - coords.p_from_fake);
result.from_percent = coords.p_from_real;
result.from = convertToValue(coords.p_from_real);
result.from_pretty = _prettify(result.from);
result.to_percent = coords.p_to_real;
result.to = convertToValue(coords.p_to_real);
result.to_pretty = _prettify(result.to);
if (options.values.length) {
result.from_value = options.values[result.from];
result.to_value = options.values[result.to];
}
}
calcMinMax();
calcLabels();
};
/**
* calculates pointer X in percent
*/
const calcPointerPercent = function () {
if (!coords.w_rs) {
coords.p_pointer = 0;
return;
}
if (coords.x_pointer < 0 || isNaN(coords.x_pointer)) {
coords.x_pointer = 0;
} else if (coords.x_pointer > coords.w_rs) {
coords.x_pointer = coords.w_rs;
}
coords.p_pointer = toFixed(coords.x_pointer / coords.w_rs * 100);
};
// TODO refactor next 2 functions
const convertToRealPercent = function (fake) {
const full = 100 - coords.p_handle;
return fake / full * 100;
};
const convertToFakePercent = function (real) {
const full = 100 - coords.p_handle;
return real / 100 * full;
};
const getHandleX = function () {
const max = 100 - coords.p_handle;
let x = toFixed(coords.p_pointer - coords.p_gap);
if (x < 0) {
x = 0;
} else if (x > max) {
x = max;
}
return x;
};
const calcHandlePercent = function () {
if (options.type === "single") {
coords.w_handle = cache.s_single.offsetWidth;
} else {
coords.w_handle = cache.s_from.offsetWidth;
}
coords.p_handle = toFixed(coords.w_handle / coords.w_rs * 100);
};
/**
* Find closest handle to pointer click
*
* @param real_x {Number}
* @returns {String}
*/
const chooseHandle = function (real_x) {
if (options.type === "single") {
return "single";
} else {
const m_point = coords.p_from_real + ((coords.p_to_real - coords.p_from_real) / 2);
if (real_x >= m_point) {
return options.to_fixed ? "from" : "to";
} else {
return options.from_fixed ? "to" : "from";
}
}
};
/**
* Measure Min and Max labels width in percent
*/
const calcMinMax = function () {
if (!coords.w_rs) {
return;
}
labels.p_min = labels.w_min / coords.w_rs * 100;
labels.p_max = labels.w_max / coords.w_rs * 100;
};
/**
* Measure labels width and X in percent
*/
const calcLabels = function () {
if (!coords.w_rs || options.hide_from_to) {
return;
}
if (options.type === "single") {
labels.w_single = cache.single.offsetWidth;
labels.p_single_fake = labels.w_single / coords.w_rs * 100;
labels.p_single_left = coords.p_single_fake + (coords.p_handle / 2) - (labels.p_single_fake / 2);
labels.p_single_left = checkEdges(labels.p_single_left, labels.p_single_fake);
} else {
labels.w_from = cache.from.offsetWidth;
labels.p_from_fake = labels.w_from / coords.w_rs * 100;
labels.p_from_left = coords.p_from_fake + (coords.p_handle / 2) - (labels.p_from_fake / 2);
labels.p_from_left = toFixed(labels.p_from_left);
labels.p_from_left = checkEdges(labels.p_from_left, labels.p_from_fake);
labels.w_to = cache.to.offsetWidth;
labels.p_to_fake = labels.w_to / coords.w_rs * 100;
labels.p_to_left = coords.p_to_fake + (coords.p_handle / 2) - (labels.p_to_fake / 2);
labels.p_to_left = toFixed(labels.p_to_left);
labels.p_to_left = checkEdges(labels.p_to_left, labels.p_to_fake);
labels.w_single = cache.single.offsetWidth;
labels.p_single_fake = labels.w_single / coords.w_rs * 100;
labels.p_single_left = ((labels.p_from_left + labels.p_to_left + labels.p_to_fake) / 2) - (labels.p_single_fake / 2);
labels.p_single_left = toFixed(labels.p_single_left);
labels.p_single_left = checkEdges(labels.p_single_left, labels.p_single_fake);
}
};
// Drawings
/**
* Main function called in request animation frame
* to update everything
*/
const updateScene = function () {
if (raf_id) {
cancelAnimationFrame(raf_id);
raf_id = null;
}
clearTimeout(update_tm);
update_tm = null;
if (!options) {
return;
}
drawHandles();
if (is_active) {
raf_id = requestAnimationFrame(updateScene);
} else {
update_tm = setTimeout(updateScene, 300);
}
};
/**
* Draw handles
*/
const drawHandles = function () {
coords.w_rs = cache.rs.offsetWidth;
if (!coords.w_rs) {
return;
}
if (coords.w_rs !== coords.w_rs_old) {
target = "base";
is_resize = true;
}
if (coords.w_rs !== coords.w_rs_old || force_redraw) {
setMinMax();
calc(true);
drawLabels();
if (options.grid) {
calcGridMargin();
calcGridLabels();
}
force_redraw = true;
coords.w_rs_old = coords.w_rs;
drawShadow();
}
if (!coords.w_rs) {
return;
}
if (!dragging && !force_redraw && !is_key) {
return;
}
if (old_from !== result.from || old_to !== result.to || force_redraw || is_key) {
drawLabels();
cache.bar.style.left = coords.p_bar_x + "%";
cache.bar.style.width = coords.p_bar_w + "%";
if (options.type === "single") {
cache.bar.style.left = "0";
cache.bar.style.width = coords.p_bar_w + coords.p_bar_x + "%";
cache.s_single.style.left = coords.p_single_fake + "%";
cache.single.style.left = labels.p_single_left + "%";
} else {
cache.s_from.style.left = coords.p_from_fake + "%";
cache.s_to.style.left = coords.p_to_fake + "%";
if (old_from !== result.from || force_redraw) {
cache.from.style.left = labels.p_from_left + "%";
}
if (old_to !== result.to || force_redraw) {
cache.to.style.left = labels.p_to_left + "%";
}
cache.single.style.left = labels.p_single_left + "%";
}
writeToInput();
if ((old_from !== result.from || old_to !== result.to) && !is_start) {
cache.input.dispatchEvent(new Event("change"));
cache.input.dispatchEvent(new Event("input"));
}
old_from = result.from;
old_to = result.to;
// callbacks call
if (!is_resize && !is_update && !is_start && !is_finish) {
callOnChange();
}
if (is_key || is_click) {
is_key = false;
is_click = false;
callOnFinish();
}
is_update = false;
is_resize = false;
is_finish = false;
}
is_start = false;
is_key = false;
is_click = false;
force_redraw = false;
};
/**
* Draw labels
* measure labels collisions
* collapse close labels
*/
const drawLabels = function () {
if (!options) {
return;
}
const values_num = options.values.length,
p_values = options.p_values;
let text_single,
text_from,
text_to,
from_pretty,
to_pretty;
if (options.hide_from_to) {
return;
}
if (options.type === "single") {
if (values_num) {
text_single = decorate(p_values[result.from]);
cache.single.innerHTML = text_single;
} else {
from_pretty = _prettify(result.from);
text_single = decorate(from_pretty, result.from);
cache.single.innerHTML = text_single;
}
calcLabels();
if (labels.p_single_left < labels.p_min + 1) {
cache.min.style.visibility = "hidden";
} else {
cache.min.style.visibility = "visible";
}
if (labels.p_single_left + labels.p_single_fake > 100 - labels.p_max - 1) {
cache.max.style.visibility = "hidden";
} else {
cache.max.style.visibility = "visible";
}
} else {
if (values_num) {
if (options.decorate_both) {
text_single = decorate(p_values[result.from]);
text_single += options.values_separator;
text_single += decorate(p_values[result.to]);
} else {
text_single = decorate(p_values[result.from] + options.values_separator + p_values[result.to]);
}
text_from = decorate(p_values[result.from]);
text_to = decorate(p_values[result.to]);
cache.single.innerHTML = text_single;
cache.from.innerHTML = text_from;
cache.to.innerHTML = text_to;
} else {
from_pretty = _prettify(result.from);
to_pretty = _prettify(result.to);
if (options.decorate_both) {
text_single = decorate(from_pretty, result.from);
text_single += options.values_separator;
text_single += decorate(to_pretty, result.to);
} else {
text_single = decorate(from_pretty + options.values_separator + to_pretty, result.to);
}
text_from = decorate(from_pretty, result.from);
text_to = decorate(to_pretty, result.to);
cache.single.innerHTML = text_single;
cache.from.innerHTML = text_from;
cache.to.innerHTML = text_to;
}
calcLabels();
const min = Math.min(labels.p_single_left, labels.p_from_left),
single_left = labels.p_single_left + labels.p_single_fake,
to_left = labels.p_to_left + labels.p_to_fake;
let max = Math.max(single_left, to_left);
if (labels.p_from_left + labels.p_from_fake >= labels.p_to_left) {
cache.from.style.visibility = "hidden";
cache.to.style.visibility = "hidden";
cache.single.style.visibility = "visible";
if (result.from === result.to) {
if (target === "from") {
cache.from.style.visibility = "visible";
} else if (target === "to") {
cache.to.style.visibility = "visible";
} else if (!target) {
cache.from.style.visibility = "visible";
}
cache.single.style.visibility = "hidden";
max = to_left;
} else {
cache.from.style.visibility = "hidden";
cache.to.style.visibility = "hidden";
cache.single.style.visibility = "visible";
max = Math.max(single_left, to_left);
}
} else {
cache.from.style.visibility = "visible";
cache.to.style.visibility = "visible";
cache.single.style.visibility = "hidden";
}
min < labels.p_min + 1 ? cache.min.style.visibility = "hidden" : cache.min.style.visibility = "visible";
max > 100 - labels.p_max - 1 ? cache.max.style.visibility = "hidden" : cache.max.style.visibility = "visible";
}
};
/**
* Draw shadow intervals
*/
const drawShadow = function () {
const o = options,
c = cache,
is_from_min = typeof o.from_min === "number" && !isNaN(o.from_min),
is_from_max = typeof o.from_max === "number" && !isNaN(o.from_max),
is_to_min = typeof o.to_min === "number" && !isNaN(o.to_min),
is_to_max = typeof o.to_max === "number" && !isNaN(o.to_max);
let from_min,
from_max,
to_min,
to_max;
if (o.type === "single") {
if (o.from_shadow && (is_from_min || is_from_max)) {
from_min = convertToPercent(is_from_min ? o.from_min : o.min);
from_max = convertToPercent(is_from_max ? o.from_max : o.max) - from_min;
from_min = toFixed(from_min - (coords.p_handle / 100 * from_min));
from_max = toFixed(from_max - (coords.p_handle / 100 * from_max));
from_min = from_min + (coords.p_handle / 2);
c.shad_single.style.display = "block";
c.shad_single.style.left = from_min + "%";
c.shad_single.style.width = from_max + "%";
} else {
c.shad_single.style.display = "none";
}
} else {
if (o.from_shadow && (is_from_min || is_from_max)) {
from_min = convertToPercent(is_from_min ? o.from_min : o.min);
from_max = convertToPercent(is_from_max ? o.from_max : o.max) - from_min;
from_min = toFixed(from_min - (coords.p_handle / 100 * from_min));
from_max = toFixed(from_max - (coords.p_handle / 100 * from_max));
from_min = from_min + (coords.p_handle / 2);
c.shad_from.style.display = "block";
c.shad_from.style.left = from_min + "%";
c.shad_from.style.width = from_max + "%";
} else {
c.shad_from.style.display = "none";
}
if (o.to_shadow && (is_to_min || is_to_max)) {
to_min = convertToPercent(is_to_min ? o.to_min : o.min);
to_max = convertToPercent(is_to_max ? o.to_max : o.max) - to_min;
to_min = toFixed(to_min - (coords.p_handle / 100 * to_min));
to_max = toFixed(to_max - (coords.p_handle / 100 * to_max));
to_min = to_min + (coords.p_handle / 2);
c.shad_to.style.display = "block";
c.shad_to.style.left = to_min + "%";
c.shad_to.style.width = to_max + "%";
} else {
c.shad_to.style.display = "none";
}
}
};
/**
* Write values to input element
*/
const writeToInput = function () {
if (options.type === "single") {
if (options.values.length) {
cache.input.setAttribute("value", result.from_value);
} else {
cache.input.setAttribute("value", result.from);
}
cache.input.dataset.from = result.from;
} else {
if (options.values.length) {
cache.input.setAttribute("value", result.from_value + options.input_values_separator + result.to_value);
} else {
cache.input.setAttribute("value", result.from + options.input_values_separator + result.to);
}
cache.input.dataset.from = result.from;
cache.input.dataset.to = result.to;
}
};
// Callbacks
const callOnStart = function () {
writeToInput();
if (options.onStart && typeof options.onStart === "function") {
if (options.scope) {
options.onStart.call(options.scope, result);
} else {
options.onStart(result);
}
}
};
const callOnChange = function () {
writeToInput();
if (options.onChange && typeof options.onChange === "function") {
if (options.scope) {
options.onChange.call(options.scope, result);
} else {
options.onChange(result);
}
}
};
const callOnFinish = function () {
writeToInput();
if (options.onFinish && typeof options.onFinish === "function") {
if (options.scope) {
options.onFinish.call(options.scope, result);
} else {
options.onFinish(result);
}
}
};
const callOnUpdate = function () {
writeToInput();
if (options.onUpdate && typeof options.onUpdate === "function") {
if (options.scope) {
options.onUpdate.call(options.scope, result);
} else {
options.onUpdate(result);
}
}
};
// Service methods
const toggleInput = function () {
cache.input.classList.toggle("irs-hidden-input");
if (has_tab_index) {
cache.input.setAttribute("tabindex", "-1");
} else {
cache.input.removeAttribute("tabindex");
}
has_tab_index = !has_tab_index;
};
/**
* Convert real value to percent
*
* @param value {Number} X in real
* @param no_min {boolean=} don't use min value
* @returns {Number} X in percent
*/
const convertToPercent = function (value, no_min) {
let diapason = options.max - options.min,
one_percent = diapason / 100,
val, percent;
if (!diapason) {
no_diapason = true;
return 0;
}
if (no_min) {
val = value;
} else {
val = value - options.min;
}
percent = val / one_percent;
return toFixed(percent);
};
/**
* Convert percent to real values
*
* @param percent {Number} X in percent
* @returns {Number} X in real
*/
const convertToValue = function (percent) {
let min = options.min,
max = options.max,
min_decimals = min.toString().split(".")[1],
max_decimals = max.toString().split(".")[1],
min_length, max_length,
avg_decimals = 0,
abs = 0;
if (percent === 0) {
return options.min;
}
if (percent === 100) {
return options.max;
}
if (min_decimals) {
min_length = min_decimals.length;
avg_decimals = min_length;
}
if (max_decimals) {
max_length = max_decimals.length;
avg_decimals = max_length;
}
if (min_length && max_length) {
avg_decimals = (min_length >= max_length) ? min_length : max_length;
}
if (min < 0) {
abs = Math.abs(min);
min = +(min + abs).toFixed(avg_decimals);
max = +(max + abs).toFixed(avg_decimals);
}
let number = ((max - min) / 100 * percent) + min,
string = options.step.toString().split(".")[1],
result;
if (string) {
number = +number.toFixed(string.length);
} else {
number = number / opti