295 lines
12 KiB
JavaScript
295 lines
12 KiB
JavaScript
/** @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.
|
|
*
|
|
* <input type="range" multiple="multiple" class="custom-range
|
|
* range-with-input" min=2 max=10 step=0.5 data-currency="€"
|
|
* data-currency-position="before" value="4,8"/>
|
|
*
|
|
* Providing a HTMLElement and an Object with the desired options.
|
|
*
|
|
* <input id="multi" type="range" class="custom-range"/>
|
|
*
|
|
* 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,
|
|
};
|