/** * 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. */ odoo.define('website_sale.multirange', function () { 'use strict'; /** * 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"); 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'; /* Wrap the input and add its ghost */ this.rangeDiv = document.createElement("div"); this.rangeDiv.classList.add("multirange-wrapper"); 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("multirange", "original"); this.ghost.classList.add("multirange", "ghost"); this.input.value = values[0] || this.min; this.ghost.value = values[1] || this.max; this.inputTipLocator = document.createElement("div"); this.inputTipLocator.classList.add("tip-locator"); this.ghostTipLocator = document.createElement("div"); this.ghostTipLocator.classList.add("tip-locator"); this.rangeDiv.insertBefore(this.ghostTipLocator, this.input.nextSibling); this.rangeDiv.insertBefore(this.inputTipLocator, this.ghost.nextSibling); this.leftCounter = document.createElement("span"); this.leftCounter.classList.add("multirange-min"); this.rightCounter = document.createElement("span"); this.rightCounter.classList.add("multirange-max"); this.tipLocatorOptions = { container: this.rangeDiv, html: true, }; $(this.inputTipLocator).popover(Object.assign( this.tipLocatorOptions, { placement: 'top', content: this.leftCounter }) ); $(this.ghostTipLocator).popover(Object.assign( this.tipLocatorOptions, { placement: 'bottom', content: this.rightCounter }) ); $(this.inputTipLocator).add($(this.ghostTipLocator)).popover('show'); /* Add the counterInput */ if (this.rangeWithInput) { this.leftInput = document.createElement("input"); this.leftInput.type = "number"; this.leftInput.style.display = "none"; 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"); this.rightInput.classList.add("multirange-max"); 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); const tipOffsetLow = 8 - (low * 0.15); const tipOffsetHigh = 8 - (high * 0.15); this.rangeDiv.style.setProperty("--low", low + '%'); this.rangeDiv.style.setProperty("--high", high + '%'); $(this.inputTipLocator).css({ 'left': `calc(${low}% + (${tipOffsetLow}px))`, 'top': '3px' }); $(this.ghostTipLocator).css({ 'left': `calc(${high}% + (${tipOffsetHigh}px))`, 'top': '18px' }); $(this.inputTipLocator).add($(this.ghostTipLocator)).popover('update'); 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.style.display === "none") { this.input.valueLow = this.leftInput.value; this.input.valueHigh = this.rightInput.value; this.dispatchNewValueEvent(); this.update(); counter.style.display = ""; input.style.display = "none"; } else { counter.style.display = "none"; input.style.display = ""; 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) { number = String(number).split('.'); if (number[1] && number[1].length === 1) { number[1] += '0'; } let formatedNumber = number[0].replace(/(?=(?:\d{3})+$)(?!\b)/g, ',') + (number[1] ? '.' + number[1] : '.00'); if (this.currency.length) { if (this.currencyPosition === 'after') { formatedNumber = formatedNumber + ' ' + this.currency; } else { formatedNumber = this.currency + ' ' + formatedNumber; } } return formatedNumber; } } function multirange(input, options) { if (input.classList.contains('multirange')) { return; } new Multirange(input, options); } return { Multirange: Multirange, init: multirange, }; }); odoo.define('website_sale.multirange.instance', function (require) { 'use strict'; const publicWidget = require('web.public.widget'); const multirange = require('website_sale.multirange'); publicWidget.registry.WebsiteMultirangeInputs = publicWidget.Widget.extend({ selector: 'input[type=range][multiple]:not(.multirange)', /** * @override */ start() { return this._super.apply(this, arguments).then(() => { multirange.init(this.el); }); }, }); });