/** @odoo-module **/ /** * This code has been more that widely inspired by the multirange library * which can be found on https://github.com/LeaVerou/multirange. * * The license file can be found in the same folder as this file. */ /** * The multirange library will display the two values as one range input with * two cursors linked by a background. This is to be used with Bootstrap * custom-range inputs. * * There is 2 number inputs on the right and left of the multirange to * display and allow quick and precise value modifications. They are * initialized with the same value provided to the input (min, max, step). * * There is 2 events that are added to the input: * - oldRangeValue: Triggered when the user clicks on a cursor or on focus * of the right or left number input. * - newRangeValue: Triggered when the user release a cursor or on focus * out of the right or left number input. * * The options available for the multirange are : * - On range input or as multirange method options: * - min: minimal value of the range. Default: 0. * - max: maximal value of the range. Default: 100. * - step: precision of the range. Default: 1. * - currency: symbol preceding the displayed values. Default: Empty. * - currencyPosition: currency before/after value. Default: "after". * - value: the current value of the range. Default: "0,100". * * - As multirange method options only: * - displayCounterInput: if we display the value. Default: true. * * Initialization of a multiple range input can be done in two ways: * * Having the inputs with the options as properties and the multiple * property set will let the library initialize it just after DOM loaded. * * * * Providing a HTMLElement and an Object with the desired options. * * * * multirange(document.querySelector('#multi'), { * min: 2, * max: 10, * step: 0.5, * currency: "€", * currencyPosition: "before", * value: "4,8" * rangeWithInput: true, * }); */ const HTMLInputElement = window.HTMLInputElement; const descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value"); export class Multirange { constructor(input, options = {}) { const self = this; /* Set default and optionnal values */ this.input = input; this.rangeWithInput = options.rangeWithInput === true || this.input.classList.contains('range-with-input'); const value = options.value || this.input.getAttribute("value"); const values = value === null ? [] : value.split(","); this.input.min = this.min = options.min || this.input.min || 0; this.input.max = this.max = options.max || this.input.max || 100; this.input.step = this.step = options.step || this.input.step || 1; this.currency = options.currency || this.input.dataset.currency || ''; this.currencyPosition = options.currencyPosition || this.input.dataset.currencyPosition || 'after'; const inputClasses = ["multirange", "d-inline-block", "p-0", "align-top"]; /* Wrap the input and add its ghost */ this.rangeDiv = document.createElement("div"); this.rangeDiv.classList.add("multirange-wrapper", "position-relative", "mb-5"); this.countersWrapper = document.createElement("small"); this.countersWrapper.classList.add("d-flex", "justify-content-between", "mt-2"); this.rangeDiv.appendChild(this.countersWrapper); this.input.parentNode.insertBefore(this.rangeDiv, this.input.nextSibling); this.rangeDiv.appendChild(this.input); this.ghost = this.input.cloneNode(); this.rangeDiv.appendChild(this.ghost); this.input.classList.add("original", "position-absolute", "w-100", "m-0", ...inputClasses); this.ghost.classList.add("ghost", "position-relative", ...inputClasses); this.input.value = values[0] || this.min; this.ghost.value = values[1] || this.max; this.leftCounter = document.createElement("span"); this.leftCounter.classList.add("multirange-min", "position-absolute", "opacity-75", "opacity-100-hover", "mt-1"); this.rightCounter = document.createElement("span"); this.rightCounter.classList.add("multirange-max", "position-absolute", "opacity-75", "opacity-100-hover", "mt-1", "end-0"); this.countersWrapper.append(this.leftCounter, this.rightCounter); /* Add the counterInput */ if (this.rangeWithInput) { this.leftInput = document.createElement("input"); this.leftInput.type = "number"; this.leftInput.classList.add("invisible", "form-control", "form-control-sm", "mb-2", "mb-lg-1"); this.leftInput.min = this.min; this.leftInput.max = this.max; this.leftInput.step = this.step; this.rightInput = this.leftInput.cloneNode(); this.leftInput.classList.add("multirange-min", "invisible"); this.rightInput.classList.add("multirange-max", "invisible"); this.leftCounter.parentNode.appendChild(this.leftInput); this.rightCounter.parentNode.appendChild(this.rightInput); } /* Define new properties on range input to link it with ghost, especially for Safari compatibility*/ Object.defineProperty(this.input, "originalValue", descriptor.get ? descriptor : { get: function () { return this.value; }, set: function (v) { this.value = v; } }); Object.defineProperties(this.input, { valueLow: { get: function () { return Math.min(this.originalValue, self.ghost.value); }, set: function (v) { this.originalValue = v; }, enumerable: true }, valueHigh: { get: function () { return Math.max(this.originalValue, self.ghost.value); }, set: function (v) { self.ghost.value = v; }, enumerable: true } }); if (descriptor.get) { Object.defineProperty(this.input, "value", { get: function () { return this.valueLow + "," + this.valueHigh; }, set: function (v) { const values = v.split(","); this.valueLow = values[0]; this.valueHigh = values[1]; this.update(); }, enumerable: true }); } if (typeof this.input.oninput === "function") { this.ghost.oninput = this.input.oninput.bind(this.input); } this.input.addEventListener("input", this.update.bind(this)); this.ghost.addEventListener("input", this.update.bind(this)); this.input.addEventListener("touchstart", this.saveOldValues.bind(this)); this.ghost.addEventListener("touchstart", this.saveOldValues.bind(this)); this.input.addEventListener("mousedown", this.saveOldValues.bind(this)); this.ghost.addEventListener("mousedown", this.saveOldValues.bind(this)); this.input.addEventListener("touchend", this.dispatchNewValueEvent.bind(this)); this.ghost.addEventListener("touchend", this.dispatchNewValueEvent.bind(this)); this.input.addEventListener("mouseup", this.dispatchNewValueEvent.bind(this)); this.ghost.addEventListener("mouseup", this.dispatchNewValueEvent.bind(this)); if (this.rangeWithInput) { this.leftCounter.addEventListener("click", this.counterInputSwitch.bind(this)); this.rightCounter.addEventListener("click", this.counterInputSwitch.bind(this)); this.leftInput.addEventListener("blur", this.counterInputSwitch.bind(this)); this.rightInput.addEventListener("blur", this.counterInputSwitch.bind(this)); this.leftInput.addEventListener("keypress", this.elementBlurOnEnter.bind(this)); this.rightInput.addEventListener("keypress", this.elementBlurOnEnter.bind(this)); this.leftInput.addEventListener("focus", this.selectAllFocus.bind(this)); this.rightInput.addEventListener("focus", this.selectAllFocus.bind(this)); } this.update(); $(this.rangeDiv).addClass('visible'); } update() { const low = 100 * (this.input.valueLow - this.min) / (this.max - this.min); const high = 100 * (this.input.valueHigh - this.min) / (this.max - this.min); this.rangeDiv.style.setProperty("--low", low + '%'); this.rangeDiv.style.setProperty("--high", high + '%'); this.counterInputUpdate(); } counterInputUpdate() { if (this.rangeWithInput) { this.leftCounter.innerText = this.formatNumber(this.input.valueLow); this.rightCounter.innerText = this.formatNumber(this.input.valueHigh); this.leftInput.value = this.input.valueLow; this.rightInput.value = this.input.valueHigh; } } counterInputSwitch(ev) { let counter = this.rightCounter; let input = this.rightInput; if (ev.currentTarget.classList.contains('multirange-min')) { counter = this.leftCounter; input = this.leftInput; } if (counter.classList.contains("invisible")) { this.input.valueLow = this.leftInput.value; this.input.valueHigh = this.rightInput.value; this.dispatchNewValueEvent(); this.update(); counter.classList.remove("invisible"); input.classList.add("invisible"); } else { counter.classList.add("invisible"); input.classList.remove("invisible"); this.saveOldValues(); // Hack because firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=1057858 window.setTimeout(function () { input.focus(); }, 1); } } elementBlurOnEnter(ev) { if (ev.key === "Enter") { ev.currentTarget.blur(); } } selectAllFocus(ev) { ev.currentTarget.select(); } dispatchNewValueEvent() { if (this._previousMaxPrice !== this.input.valueHigh || this._previousMinPrice !== this.input.valueLow) { this.input.dispatchEvent(new CustomEvent("newRangeValue", { bubbles: true, })); } } saveOldValues() { this._previousMinPrice = this.input.valueLow; this._previousMaxPrice = this.input.valueHigh; } formatNumber(number) { const language = document.querySelector("html").getAttribute("lang"); const locale = language === "sr@latin" ? "sr-Latn-RS" : language.replace(/_/g, "-"); let formatedNumber = number.toLocaleString(locale, { minimumFractionDigits: 2, maximumFractionDigits: 2, }); if (this.currency.length) { if (this.currencyPosition === 'after') { formatedNumber = formatedNumber + ' ' + this.currency; } else { formatedNumber = this.currency + ' ' + formatedNumber; } } return formatedNumber; } } export function multirange(input, options) { if (input.classList.contains('multirange')) { return; } new Multirange(input, options); } export default { Multirange: Multirange, init: multirange, };