diff --git a/addons/account/static/src/js/account_portal_sidebar.js b/addons/account/static/src/js/account_portal_sidebar.js index 5d32f06c086..25ce665e75b 100644 --- a/addons/account/static/src/js/account_portal_sidebar.js +++ b/addons/account/static/src/js/account_portal_sidebar.js @@ -3,7 +3,6 @@ import dom from "@web/legacy/js/core/dom"; import publicWidget from "@web/legacy/js/public/public_widget"; import PortalSidebar from "@portal/js/portal_sidebar"; -import utils from "@web/legacy/js/core/utils"; publicWidget.registry.AccountPortalSidebar = PortalSidebar.extend({ selector: '.o_portal_invoice_sidebar', @@ -50,7 +49,8 @@ publicWidget.registry.AccountPortalSidebar = PortalSidebar.extend({ $el.height($wrapwrap[0].scrollHeight); // scroll to the right place after iframe resize - if (!utils.isValidAnchor(window.location.hash)) { + const isAnchor = /^#[\w-]+$/.test(window.location.hash) + if (!isAnchor) { return; } var $target = $(window.location.hash); diff --git a/addons/web/static/src/core/colorpicker/colorpicker.js b/addons/web/static/src/core/colorpicker/colorpicker.js index 089a3023f51..cd0fb4bf581 100644 --- a/addons/web/static/src/core/colorpicker/colorpicker.js +++ b/addons/web/static/src/core/colorpicker/colorpicker.js @@ -5,7 +5,10 @@ import { convertRgbaToCSSColor, convertRgbToHsl, convertHslToRgb, -} from '@web/core/utils/colors'; +} from "@web/core/utils/colors"; +import { uniqueId } from "@web/core/utils/functions"; +import { clamp } from "@web/core/utils/numbers"; +import { throttleForAnimation } from "@web/core/utils/timing"; import { Component, @@ -15,25 +18,9 @@ import { onWillUpdateProps, onWillDestroy, } from "@odoo/owl"; -import { uniqueId } from "@web/core/utils/functions"; -import { throttleForAnimation } from "@web/core/utils/timing"; - - -/** - * Confines a value inside an interval - * - * @param {number} [val] the value to confine - * @param {number} [min] the minimum of the interval - * @param {number} [max] the maximum of the interval - * @return {number} val if val is in [min, max], min if val < min and max - * otherwise - */ -function confine(val, min, max) { - return Math.max(min, Math.min(max, val)); -} export class Colorpicker extends Component { - static template = 'web.Colorpicker'; + static template = "web.Colorpicker"; static props = { document: { type: true, optional: true }, defaultColor: { type: String, optional: true }, @@ -47,7 +34,7 @@ export class Colorpicker extends Component { }; static defaultProps = { document: window.document, - defaultColor: '#FF0000', + defaultColor: "#FF0000", noTransparency: false, colorPreview: false, stopClickPropagation: false, @@ -56,7 +43,7 @@ export class Colorpicker extends Component { onInputEnter: () => {}, }; - elRef = useRef('el'); + elRef = useRef("el"); setup() { onWillStart(() => { @@ -71,7 +58,7 @@ export class Colorpicker extends Component { // `inOwnerDocument`). // Todo: this workaround should be removed when the snippet menu is // converted to owl. - await new Promise(resolve => { + await new Promise((resolve) => { const observer = new MutationObserver(() => { if (this.elRef.el) { observer.disconnect(); @@ -84,12 +71,16 @@ export class Colorpicker extends Component { this.el = this.elRef.el; this.$el = $(this.el); - this.$el.on('click', this._onClick.bind(this)); - this.$el.on('keypress', this._onKeypress.bind(this)); - this.$el.on('mousedown', '.o_color_pick_area', this._onMouseDownPicker.bind(this)); - this.$el.on('mousedown', '.o_color_slider', this._onMouseDownSlider.bind(this)); - this.$el.on('mousedown', '.o_opacity_slider', this._onMouseDownOpacitySlider.bind(this)); - this.$el.on('change', '.o_color_picker_inputs', this._onChangeInputs.bind(this)); + this.$el.on("click", this._onClick.bind(this)); + this.$el.on("keypress", this._onKeypress.bind(this)); + this.$el.on("mousedown", ".o_color_pick_area", this._onMouseDownPicker.bind(this)); + this.$el.on("mousedown", ".o_color_slider", this._onMouseDownSlider.bind(this)); + this.$el.on( + "mousedown", + ".o_opacity_slider", + this._onMouseDownOpacitySlider.bind(this) + ); + this.$el.on("change", ".o_color_picker_inputs", this._onChangeInputs.bind(this)); this.start(); }); @@ -109,18 +100,18 @@ export class Colorpicker extends Component { this.opacitySliderFlag = false; this.colorComponents = {}; this.uniqueId = uniqueId("colorpicker"); - this.selectedHexValue = ''; + this.selectedHexValue = ""; } /** * @override */ start() { - this.$colorpickerArea = this.$el.find('.o_color_pick_area'); - this.$colorpickerPointer = this.$el.find('.o_picker_pointer'); - this.$colorSlider = this.$el.find('.o_color_slider'); - this.$colorSliderPointer = this.$el.find('.o_slider_pointer'); - this.$opacitySlider = this.$el.find('.o_opacity_slider'); - this.$opacitySliderPointer = this.$el.find('.o_opacity_pointer'); + this.$colorpickerArea = this.$el.find(".o_color_pick_area"); + this.$colorpickerPointer = this.$el.find(".o_picker_pointer"); + this.$colorSlider = this.$el.find(".o_color_slider"); + this.$colorSliderPointer = this.$el.find(".o_slider_pointer"); + this.$opacitySlider = this.$el.find(".o_opacity_slider"); + this.$opacitySliderPointer = this.$el.find(".o_opacity_pointer"); const rgba = convertCSSColorToRgba(this.props.defaultColor); if (rgba) { @@ -150,15 +141,20 @@ export class Colorpicker extends Component { // Need to be bound on all documents to work in all possible cases (we // have to be able to start dragging/moving from the colorpicker to // anywhere on the screen, crossing iframes). - this.$documents = $([window.top, ...Array.from(window.top.frames).filter(frame => { - try { - const document = frame.document; - return !!document; - } catch { - // We cannot access the document (cross origin). - return false; - } - })].map(w => w.document)); + this.$documents = $( + [ + window.top, + ...Array.from(window.top.frames).filter((frame) => { + try { + const document = frame.document; + return !!document; + } catch { + // We cannot access the document (cross origin). + return false; + } + }), + ].map((w) => w.document) + ); this.throttleOnMouseMove = throttleForAnimation((ev) => { this._onMouseMovePicker(ev); this._onMouseMoveSlider(ev); @@ -217,33 +213,36 @@ export class Colorpicker extends Component { } // Update preview - this.$el.find('.o_color_preview').css('background-color', this.colorComponents.cssColor); + this.$el.find(".o_color_preview").css("background-color", this.colorComponents.cssColor); // Update picker area and picker pointer position this.$colorpickerArea.css( "background-color", `hsl(${this.colorComponents.hue}, 100%, 50%)` ); - const top = (100 - this.colorComponents.lightness) * this.$colorpickerArea.height() / 100; - const left = this.colorComponents.saturation * this.$colorpickerArea.width() / 100; + const top = ((100 - this.colorComponents.lightness) * this.$colorpickerArea.height()) / 100; + const left = (this.colorComponents.saturation * this.$colorpickerArea.width()) / 100; this.$colorpickerPointer.css({ - top: (top - 5) + 'px', - left: (left - 5) + 'px', + top: top - 5 + "px", + left: left - 5 + "px", }); // Update color slider position const height = this.$colorSlider.height(); - const y = this.colorComponents.hue * height / 360; - this.$colorSliderPointer.css('top', Math.round(y - 2)); + const y = (this.colorComponents.hue * height) / 360; + this.$colorSliderPointer.css("top", Math.round(y - 2)); if (!this.props.noTransparency) { // Update opacity slider position const heightOpacity = this.$opacitySlider.height(); const z = heightOpacity * (1 - this.colorComponents.opacity / 100.0); - this.$opacitySliderPointer.css('top', Math.round(z - 2)); + this.$opacitySliderPointer.css("top", Math.round(z - 2)); // Add gradient color on opacity slider - this.$opacitySlider.css('background', 'linear-gradient(' + this.colorComponents.hex + ' 0%, transparent 100%)'); + this.$opacitySlider.css( + "background", + "linear-gradient(" + this.colorComponents.hex + " 0%, transparent 100%)" + ); } } /** @@ -257,8 +256,9 @@ export class Colorpicker extends Component { if (!rgb) { return; } - Object.assign(this.colorComponents, - {hex: hex}, + Object.assign( + this.colorComponents, + { hex: hex }, rgb, convertRgbToHsl(rgb.red, rgb.green, rgb.blue) ); @@ -287,10 +287,11 @@ export class Colorpicker extends Component { if (!hex) { return; } - Object.assign(this.colorComponents, - {red: r, green: g, blue: b}, - a === undefined ? {} : {opacity: a}, - {hex: hex}, + Object.assign( + this.colorComponents, + { red: r, green: g, blue: b }, + a === undefined ? {} : { opacity: a }, + { hex: hex }, convertRgbToHsl(r, g, b) ); this._updateCssColor(); @@ -316,11 +317,12 @@ export class Colorpicker extends Component { } // We receive an hexa as we ignore the opacity const hex = convertRgbaToCSSColor(rgb.red, rgb.green, rgb.blue); - Object.assign(this.colorComponents, - {hue: h, saturation: s, lightness: l}, + Object.assign( + this.colorComponents, + { hue: h, saturation: s, lightness: l }, rgb, - {hex: hex}, - {opacity: a}, + { hex: hex }, + { opacity: a } ); this._updateCssColor(); } @@ -334,9 +336,7 @@ export class Colorpicker extends Component { if (a < 0 || a > 100) { return; } - Object.assign(this.colorComponents, - {opacity: a} - ); + Object.assign(this.colorComponents, { opacity: a }); this._updateCssColor(); } /** @@ -357,9 +357,7 @@ export class Colorpicker extends Component { const g = this.colorComponents.green; const b = this.colorComponents.blue; const a = this.colorComponents.opacity; - Object.assign(this.colorComponents, - {cssColor: convertRgbaToCSSColor(r, g, b, a)} - ); + Object.assign(this.colorComponents, { cssColor: convertRgbaToCSSColor(r, g, b, a) }); if (this.previewActive) { this.props.onColorPreview(this.colorComponents); } @@ -375,7 +373,7 @@ export class Colorpicker extends Component { */ _onKeypress(ev) { if (ev.charCode === $.ui.keyCode.ENTER) { - if (ev.target.tagName === 'INPUT') { + if (ev.target.tagName === "INPUT") { this._onChangeInputs(ev); } ev.preventDefault(); @@ -390,13 +388,16 @@ export class Colorpicker extends Component { ev.stopPropagation(); } ev.originalEvent.__isColorpickerClick = true; - $(ev.target).find('> .o_opacity_pointer, > .o_slider_pointer, > .o_picker_pointer').addBack('.o_opacity_pointer, .o_slider_pointer, .o_picker_pointer').focus(); - if (ev.target.dataset.colorMethod === 'hex' && !this.selectedHexValue) { + $(ev.target) + .find("> .o_opacity_pointer, > .o_slider_pointer, > .o_picker_pointer") + .addBack(".o_opacity_pointer, .o_slider_pointer, .o_picker_pointer") + .focus(); + if (ev.target.dataset.colorMethod === "hex" && !this.selectedHexValue) { ev.target.select(); this.selectedHexValue = ev.target.value; return; } - this.selectedHexValue = ''; + this.selectedHexValue = ""; } /** * Updates color when the user starts clicking on the picker. @@ -423,10 +424,12 @@ export class Colorpicker extends Component { const offset = this.$colorpickerArea.offset(); const top = ev.pageY - offset.top; const left = ev.pageX - offset.left; - let saturation = Math.round(100 * left / this.$colorpickerArea.width()); - let lightness = Math.round(100 * (this.$colorpickerArea.height() - top) / this.$colorpickerArea.height()); - saturation = confine(saturation, 0, 100); - lightness = confine(lightness, 0, 100); + let saturation = Math.round((100 * left) / this.$colorpickerArea.width()); + let lightness = Math.round( + (100 * (this.$colorpickerArea.height() - top)) / this.$colorpickerArea.height() + ); + saturation = clamp(saturation, 0, 100); + lightness = clamp(lightness, 0, 100); this._updateHsl(this.colorComponents.hue, saturation, lightness); this._updateUI(); @@ -454,8 +457,8 @@ export class Colorpicker extends Component { } const y = ev.pageY - this.$colorSlider.offset().top; - let hue = Math.round(360 * y / this.$colorSlider.height()); - hue = confine(hue, 0, 360); + let hue = Math.round((360 * y) / this.$colorSlider.height()); + hue = clamp(hue, 0, 360); this._updateHsl(hue, this.colorComponents.saturation, this.colorComponents.lightness); this._updateUI(); @@ -484,7 +487,7 @@ export class Colorpicker extends Component { const y = ev.pageY - this.$opacitySlider.offset().top; let opacity = Math.round(100 * (1 - y / this.$opacitySlider.height())); - opacity = confine(opacity, 0, 100); + opacity = clamp(opacity, 0, 100); this._updateOpacity(opacity); this._updateUI(); @@ -497,26 +500,26 @@ export class Colorpicker extends Component { * @param {Event} ev */ _onChangeInputs(ev) { - switch ($(ev.target).data('colorMethod')) { - case 'hex': - this._updateHex(this.$el.find('.o_hex_input').val()); + switch ($(ev.target).data("colorMethod")) { + case "hex": + this._updateHex(this.$el.find(".o_hex_input").val()); break; - case 'rgb': + case "rgb": this._updateRgba( - parseInt(this.$el.find('.o_red_input').val()), - parseInt(this.$el.find('.o_green_input').val()), - parseInt(this.$el.find('.o_blue_input').val()) + parseInt(this.$el.find(".o_red_input").val()), + parseInt(this.$el.find(".o_green_input").val()), + parseInt(this.$el.find(".o_blue_input").val()) ); break; - case 'hsl': + case "hsl": this._updateHsl( - parseInt(this.$el.find('.o_hue_input').val()), - parseInt(this.$el.find('.o_saturation_input').val()), - parseInt(this.$el.find('.o_lightness_input').val()) + parseInt(this.$el.find(".o_hue_input").val()), + parseInt(this.$el.find(".o_saturation_input").val()), + parseInt(this.$el.find(".o_lightness_input").val()) ); break; - case 'opacity': - this._updateOpacity(parseInt(this.$el.find('.o_opacity_input').val())); + case "opacity": + this._updateOpacity(parseInt(this.$el.find(".o_opacity_input").val())); break; } this._updateUI(); diff --git a/addons/web/static/src/core/utils/binary.js b/addons/web/static/src/core/utils/binary.js index 1e033f9a5a2..aa793dbc77a 100644 --- a/addons/web/static/src/core/utils/binary.js +++ b/addons/web/static/src/core/utils/binary.js @@ -1,5 +1,7 @@ /** @odoo-module **/ +import { _t } from "@web/core/l10n/translation"; + /** * @param {string} value * @returns {boolean} @@ -16,3 +18,17 @@ export function isBinarySize(value) { export function toBase64Length(maxBytes) { return Math.ceil(maxBytes * 4 / 3); } + +/** + * @param {number} size number of bytes + * @param {string} + */ +export function humanSize(size) { + const units = _t("Bytes|Kb|Mb|Gb|Tb|Pb|Eb|Zb|Yb").split("|"); + let i = 0; + while (size >= 1024) { + size /= 1024; + ++i; + } + return `${size.toFixed(2)} ${units[i].trim()}`; +} diff --git a/addons/web/static/src/core/utils/strings.js b/addons/web/static/src/core/utils/strings.js index 20699dcd805..010adbd515d 100644 --- a/addons/web/static/src/core/utils/strings.js +++ b/addons/web/static/src/core/utils/strings.js @@ -363,3 +363,13 @@ export function unaccent(str, caseSensitive) { }); return caseSensitive ? str : str.toLowerCase(); } + +/** + * @param {string} value + * @returns boolean + */ +export function isEmail(value) { + // http://stackoverflow.com/questions/46155/validate-email-address-in-javascript + const re = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; + return re.test(value); +} diff --git a/addons/web/static/src/legacy/js/core/mixins.js b/addons/web/static/src/legacy/js/core/mixins.js index 1b82836b885..c4b1a760e44 100644 --- a/addons/web/static/src/legacy/js/core/mixins.js +++ b/addons/web/static/src/legacy/js/core/mixins.js @@ -1,7 +1,7 @@ /** @odoo-module **/ import Class from "@web/legacy/js/core/class"; -import utils from "@web/legacy/js/core/utils"; +import { floatIsZero } from "@web/core/utils/numbers"; /** * Mixin to structure objects' life-cycles folowing a parent-children @@ -391,7 +391,7 @@ var PropertiesMixin = Object.assign({}, EventDispatcherMixin, { if (key === 'value' && self.field && self.field.type === 'float' && tmp && val){ var digits = self.field.digits; if (Array.isArray(digits)) { - if (utils.float_is_zero(tmp - val, digits[1])) { + if (floatIsZero(tmp - val, digits[1])) { return; } } diff --git a/addons/web/static/src/legacy/js/core/time.js b/addons/web/static/src/legacy/js/core/time.js index afe12647269..647a70dc1e7 100644 --- a/addons/web/static/src/legacy/js/core/time.js +++ b/addons/web/static/src/legacy/js/core/time.js @@ -1,11 +1,31 @@ /** @odoo-module **/ import { localization } from "@web/core/l10n/localization"; -import utils from "@web/legacy/js/core/utils"; import { _t } from "@web/core/l10n/translation"; -var lpad = utils.lpad; -var rpad = utils.rpad; +/** + * Left-pad provided arg 1 with zeroes until reaching size provided by second + * argument. + * + * @param {number|string} str value to pad + * @param {number} size size to reach on the final padded value + * @returns {string} padded string + */ +function lpad(str, size) { + str = "" + str; + return new Array(size - str.length + 1).join('0') + str; +} +/** + * @see lpad + * + * @param {string} str + * @param {number} size + * @returns {string} + */ +function rpad(str, size) { + str = "" + str; + return str + new Array(size - str.length + 1).join('0'); +} /** * Replacer function for JSON.stringify, serializes Date objects to UTC @@ -57,7 +77,7 @@ export function str_to_datetime (str) { tmp.setUTCMinutes(parseFloat(res[5])); tmp.setUTCSeconds(parseFloat(res[6])); tmp.setUTCSeconds(parseFloat(res[6])); - tmp.setUTCMilliseconds(parseFloat(utils.rpad((res[7] || "").slice(0, 3), 3))); + tmp.setUTCMilliseconds(parseFloat(rpad((res[7] || "").slice(0, 3), 3))); return tmp; } diff --git a/addons/web/static/src/legacy/js/core/utils.js b/addons/web/static/src/legacy/js/core/utils.js index d1cb6c93807..e6d23079d39 100644 --- a/addons/web/static/src/legacy/js/core/utils.js +++ b/addons/web/static/src/legacy/js/core/utils.js @@ -6,82 +6,7 @@ * Various generic utility functions */ -import { _t } from "@web/core/l10n/translation"; -import { localization } from "@web/core/l10n/localization"; - -import { Component } from "@odoo/owl"; -import { escape, escapeMethod, sprintf as str_sprtinf } from "@web/core/utils/strings"; - -var id = -1; - -/** - * Helper function returning an extraction handler to use on array elements to - * return a certain attribute or mutated form of the element. - * - * @private - * @param {string | function} criterion - * @returns {(element: any) => any} - */ -function _getExtractorFrom(criterion) { - if (criterion) { - switch (typeof criterion) { - case 'string': return element => element[criterion]; - case 'function': return criterion; - default: throw new Error( - `Expected criterion of type 'string' or 'function' and got '${typeof criterion}'` - ); - } - } else { - return element => element; - } -} - -function boolMatch(s, matchers) { - var i, - matcher, - down = s.toLowerCase(); - matchers = [].concat(matchers); - for (i = 0; i < matchers.length; i += 1) { - matcher = matchers[i]; - if (!matcher) { - continue; - } - if (matcher.test && matcher.test(s)) { - return true; - } - if (matcher.toLowerCase() === down) { - return true; - } - } -} - -function toBoolean(str, trueValues, falseValues) { - if (typeof str === "number") { - str = ("" + str).trim(); - } - if (typeof str !== "string") { - return !!str; - } - if (boolMatch(str, trueValues || ["true", "1"])) { - return true; - } - if (boolMatch(str, falseValues || ["false", "0"])) { - return false; - } -} - -class AlreadyDefinedPatchError extends Error { - constructor() { - super(...arguments); - this.name = 'AlreadyDefinedPatchError'; - } -} -class UnknownPatchError extends Error { - constructor() { - super(...arguments); - this.name = 'UnknownPatchError'; - } -} +import { escape, escapeMethod } from "@web/core/utils/strings"; // notable issues: // * objects can't be negative in JS, so !!"" -> false but @@ -148,708 +73,7 @@ export function Markup(v, ...exprs) { } const utils = { - AlreadyDefinedPatchError, - UnknownPatchError, Markup, - - /** - * Throws an error if the given condition is not true - * - * @param {any} bool - */ - assert: function (bool) { - if (!bool) { - throw new Error("AssertionError"); - } - }, - /** - * Check if the value is a bin_size or not. - * If not, compute an approximate size out of the base64 encoded string. - * - * @param {string} value original format - * @return {string} bin_size (human-readable) - */ - binaryToBinsize: function (value) { - if (!this.is_bin_size(value)) { - // Computing approximate size out of base64 encoded string - // http://en.wikipedia.org/wiki/Base64#MIME - return this.human_size(value.length / 1.37); - } - // already bin_size - return value; - }, - /** - * Confines a value inside an interval - * - * @param {number} [val] the value to confine - * @param {number} [min] the minimum of the interval - * @param {number} [max] the maximum of the interval - * @return {number} val if val is in [min, max], min if val < min and max - * otherwise - */ - confine: function (val, min, max) { - return Math.max(min, Math.min(max, val)); - }, - /** - * Looks through the list and returns the first value that matches all - * of the key-value pairs listed in properties. - * If no match is found, or if list is empty, undefined will be returned. - * - * @param {Array} list - * @param {Object} props - * @returns {any|undefined} first element in list that matches all props - */ - findWhere: function (list, props) { - if (!Array.isArray(list) || !props) { - return; - } - return list.filter((item) => item !== undefined).find((item) => { - return Object.keys(props).every((key) => { - return item[key] === props[key]; - }) - }); - }, - /** - * @param {number} value - * @param {integer} decimals - * @returns {boolean} - */ - float_is_zero: function (value, decimals) { - var epsilon = Math.pow(10, -decimals); - return Math.abs(utils.round_precision(value, epsilon)) < epsilon; - }, - /** - * Generate a unique numerical ID - * - * @returns {integer} - */ - generateID: function () { - return ++id; - }, - /** - * Gets dataURL (base64 data) from the given file or blob. - * Technically wraps FileReader.readAsDataURL in Promise. - * - * @param {Blob|File} file - * @returns {Promise} resolved with the dataURL, or rejected if the file is - * empty or if an error occurs. - */ - getDataURLFromFile: function (file) { - if (!file) { - return Promise.reject(); - } - return new Promise(function (resolve, reject) { - var reader = new FileReader(); - reader.addEventListener('load', function () { - // Handle Chrome bug that creates invalid data URLs for empty files - if (reader.result === "data:") { - resolve(`data:${file.type};base64,`); - } else { - resolve(reader.result); - } - }); - reader.addEventListener('abort', reject); - reader.addEventListener('error', reject); - reader.readAsDataURL(file); - }); - }, - /** - * Returns an object holding different groups defined by a given criterion - * or a default one. Each group is a subset of the original given list. - * The given criterion can either be: - * - a string: a property name on the list elements which value will be the - * group name, - * - a function: a handler that will return the group name from a given - * element. - * - * @param {any[]} list - * @param {string | function} [criterion] - * @returns {Object} - */ - groupBy: function (list, criterion) { - const extract = _getExtractorFrom(criterion); - const groups = {}; - for (const element of list) { - const group = String(extract(element)); - if (!(group in groups)) { - groups[group] = []; - } - groups[group].push(element); - } - return groups; - }, - /** - * Returns a human readable number (e.g. 34000 -> 34k). - * - * @param {number} number - * @param {integer} [decimals=0] - * maximum number of decimals to use in human readable representation - * @param {integer} [minDigits=1] - * the minimum number of digits to preserve when switching to another - * level of thousands (e.g. with a value of '2', 4321 will still be - * represented as 4321 otherwise it will be down to one digit (4k)) - * @param {function} [formatterCallback] - * a callback to transform the final number before adding the - * thousands symbol (default to adding thousands separators (useful - * if minDigits > 1)) - * @returns {string} - */ - human_number: function (number, decimals, minDigits, formatterCallback) { - number = Math.round(number); - decimals = decimals | 0; - minDigits = minDigits || 1; - formatterCallback = formatterCallback || utils.insert_thousand_seps; - - var d2 = Math.pow(10, decimals); - var val = _t('kMGTPE'); - var symbol = ''; - var numberMagnitude = number.toExponential().split('e')[1]; - // the case numberMagnitude >= 21 corresponds to a number - // better expressed in the scientific format. - if (numberMagnitude >= 21) { - // we do not use number.toExponential(decimals) because we want to - // avoid the possible useless O decimals: 1e.+24 preferred to 1.0e+24 - number = Math.round(number * Math.pow(10, decimals - numberMagnitude)) / d2; - // formatterCallback seems useless here. - return number + 'e' + numberMagnitude; - } - var sign = Math.sign(number); - number = Math.abs(number); - for (var i = val.length; i > 0 ; i--) { - var s = Math.pow(10, i * 3); - if (s <= number / Math.pow(10, minDigits - 1)) { - number = Math.round(number * d2 / s) / d2; - symbol = val[i - 1]; - break; - } - } - number = sign * number; - return formatterCallback('' + number) + symbol; - }, - /** - * Returns a human readable size - * - * @param {Number} size number of bytes - */ - human_size: function (size) { - var units = _t("Bytes|Kb|Mb|Gb|Tb|Pb|Eb|Zb|Yb").split('|'); - var i = 0; - while (size >= 1024) { - size /= 1024; - ++i; - } - return size.toFixed(2) + ' ' + units[i].trim(); - }, - /** - * Insert "thousands" separators in the provided number (which is actually - * a string) - * - * @param {String} num - * @returns {String} - */ - insert_thousand_seps: function (num) { - var negative = num[0] === '-'; - num = (negative ? num.slice(1) : num); - return (negative ? '-' : '') + utils.intersperse( - num, localization.grouping, localization.thousandsSep); - }, - /** - * Intersperses ``separator`` in ``str`` at the positions indicated by - * ``indices``. - * - * ``indices`` is an array of relative offsets (from the previous insertion - * position, starting from the end of the string) at which to insert - * ``separator``. - * - * There are two special values: - * - * ``-1`` - * indicates the insertion should end now - * ``0`` - * indicates that the previous section pattern should be repeated (until all - * of ``str`` is consumed) - * - * @param {String} str - * @param {Array} indices - * @param {String} separator - * @returns {String} - */ - intersperse: function (str, indices, separator) { - separator = separator || ''; - var result = [], last = str.length; - - for(var i=0; i stop - break; - } else if (section === 0) { - // repeat previous section forever - //noinspection AssignmentToForLoopParameterJS - section = indices[--i]; - } - result.push(str.substring(last-section, last)); - last -= section; - } - - var s = str.substring(0, last); - if (s) { result.push(s); } - return result.reverse().join(separator); - }, - /** - * @param {any} object - * @param {any} path - * @returns - */ - into: function (object, path) { - if (!Array.isArray(path)) { - path = path.split('.'); - } - for (var i = 0; i < path.length; i++) { - object = object[path[i]]; - } - return object; - }, - /** - * @param {string} v - * @returns {boolean} - */ - is_bin_size: function (v) { - return (/^\d+(\.\d*)? [^0-9]+$/).test(v); - }, - /** - * Checks if a class is an extension of Component. - * - * @param {any} value A class reference - */ - isComponent: function (value) { - return value.prototype instanceof Component; - }, - /** - * Checks if a keyboard event concerns - * the numpad decimal separator key. - * - * Some countries may emit a comma instead - * of a period when this key get pressed. - * More info: https://www.iso.org/schema/isosts/v1.0/doc/n-cdf0.html - * - * @param {KeyboardEvent} ev - * @returns {boolean} - */ - isNumpadDecimalSeparatorKey(ev) { - return ['.', ','].includes(ev.key) && ev.code === 'NumpadDecimal'; - }, - /** - * Returns whether the given anchor is valid. - * - * This test is useful to prevent a crash that would happen if using an invalid - * anchor as a selector. - * - * @param {string} anchor - * @returns {boolean} - */ - isValidAnchor: function (anchor) { - return /^#[\w-]+$/.test(anchor); - }, - /** - * @param {any} node - * @param {any} human_readable - * @param {any} indent - * @returns {string} - */ - json_node_to_xml: function (node, human_readable, indent) { - // For debugging purpose, this function will convert a json node back to xml - indent = indent || 0; - var sindent = (human_readable ? (new Array(indent + 1).join('\t')) : ''), - r = sindent + '<' + node.tag, - cr = human_readable ? '\n' : ''; - - if (typeof(node) === 'string') { - return sindent + node.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); - } else if (typeof(node.tag) !== 'string' || node.children && !(node.children instanceof Array) || node.attrs && !(node.attrs instanceof Object)) { - throw new Error( - str_sprtinf(_t("Node [%s] is not a JSONified XML node"), - JSON.stringify(node))); - } - for (var attr in node.attrs) { - var vattr = node.attrs[attr]; - if (typeof(vattr) !== 'string') { - // domains, ... - vattr = JSON.stringify(vattr); - } - vattr = vattr.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); - if (human_readable) { - vattr = vattr.replace(/"/g, "'"); - } - r += ' ' + attr + '="' + vattr + '"'; - } - if (node.children && node.children.length) { - r += '>' + cr; - var childs = []; - for (var i = 0, ii = node.children.length; i < ii; i++) { - childs.push(utils.json_node_to_xml(node.children[i], human_readable, indent + 1)); - } - r += childs.join(cr); - r += cr + sindent + ''; - return r; - } else { - return r + '/>'; - } - }, - /** - * Left-pad provided arg 1 with zeroes until reaching size provided by second - * argument. - * - * @see rpad - * - * @param {number|string} str value to pad - * @param {number} size size to reach on the final padded value - * @returns {string} padded string - */ - lpad: function (str, size) { - str = "" + str; - return new Array(size - str.length + 1).join('0') + str; - }, - /** - * @param {any[]} arr - * @param {Function} fn - * @returns {any[]} - */ - partitionBy(arr, fn) { - let lastGroup = false; - let lastValue; - return arr.reduce((acc, cur) => { - let curVal = fn(cur); - if (lastGroup) { - if (curVal === lastValue) { - lastGroup.push(cur); - } else { - lastGroup = false; - } - } - if (!lastGroup) { - lastGroup = [cur]; - acc.push(lastGroup); - } - lastValue = curVal; - return acc; - }, []); - }, - /** - * performs a half up rounding with a fixed amount of decimals, correcting for float loss of precision - * See the corresponding float_round() in server/tools/float_utils.py for more info - * @param {Number} value the value to be rounded - * @param {Number} decimals the number of decimals. eg: round_decimals(3.141592,2) -> 3.14 - */ - round_decimals: function (value, decimals) { - /** - * The following decimals introduce numerical errors: - * Math.pow(10, -4) = 0.00009999999999999999 - * Math.pow(10, -5) = 0.000009999999999999999 - * - * Such errors will propagate in round_precision and lead to inconsistencies between Python - * and JavaScript. To avoid this, we parse the scientific notation. - */ - return utils.round_precision(value, parseFloat('1e' + -decimals)); - }, - /** - * performs a half up rounding with arbitrary precision, correcting for float loss of precision - * See the corresponding float_round() in server/tools/float_utils.py for more info - * - * @param {number} value the value to be rounded - * @param {number} precision a precision parameter. eg: 0.01 rounds to two digits. - */ - round_precision: function (value, precision) { - if (!value) { - return 0; - } else if (!precision || precision < 0) { - precision = 1; - } - var normalized_value = value / precision; - var epsilon_magnitude = Math.log(Math.abs(normalized_value))/Math.log(2); - var epsilon = Math.pow(2, epsilon_magnitude - 52); - normalized_value += normalized_value >= 0 ? epsilon : -epsilon; - - /** - * Javascript performs strictly the round half up method, which is asymmetric. However, in - * Python, the method is symmetric. For example: - * - In JS, Math.round(-0.5) is equal to -0. - * - In Python, round(-0.5) is equal to -1. - * We want to keep the Python behavior for consistency. - */ - var sign = normalized_value < 0 ? -1.0 : 1.0; - var rounded_value = sign * Math.round(Math.abs(normalized_value)); - return rounded_value * precision; - }, - /** - * @see lpad - * - * @param {string} str - * @param {number} size - * @returns {string} - */ - rpad: function (str, size) { - str = "" + str; - return str + new Array(size - str.length + 1).join('0'); - }, - /** - * Return a shallow copy of a given array sorted by a given criterion or a default one. - * The given criterion can either be: - * - a string: a property name on the array elements returning the sortable primitive - * - a function: a handler that will return the sortable primitive from a given element. - * - * @param {any[]} array - * @param {string | function} [criterion] - * @param {('asc' | 'desc')} [order='asc'] sort by ascending if order is 'asc' else descending - */ - sortBy: function (array, criterion, order = 'asc') { - const extract = _getExtractorFrom(criterion); - return array.slice().sort((elA, elB) => { - const a = extract(elA); - const b = extract(elB); - let result; - if (isNaN(a) && isNaN(b)) { - result = a > b ? 1 : a < b ? -1 : 0; - } else { - result = a - b; - } - return order === 'asc' ? result : -result; - }); - }, - /** - * Returns a string formatted using given values. - * - * If the value is an object, its keys will replace `%(key)s` expressions. - * If the values are a set of strings, they will replace `%s` expressions. - * If no value is given, the string will not be formatted. - * - * `Markup`-aware: if a `Markup` object is provided as format string, - * automatically escapes injected values and returns a new `Markup` - * object. - * - * @param {string|Markup} string format string - * @param values values injected into the format string, can be either a - * sequence of positional parameters (for positional - * placeholders) or a single Object acting a as map (for named - * placeholders). - * @returns {string|Markup} - */ - sprintf(string, ...values) { - let finalizer, mapper; - finalizer = mapper = a => a; - if (string instanceof _Markup) { - string = String(string); - finalizer = Markup; - mapper = escape; - } - if (values.length === 1 && typeof values[0] === 'object') { - const valuesDict = values[0]; - string = string.replace( - /%\((\w+)\)s/g, - (_, group) => mapper(valuesDict[group])); - } else { - let i = 0; - string = string.replace(/%s/g, () => mapper(values[i++])); - } - return finalizer(string); - }, - /** - * Sort an array in place, keeping the initial order for identical values. - * - * @param {Array} array - * @param {function} iteratee - */ - stableSort: function (array, iteratee) { - var stable = array.slice(); - return array.sort(function stableCompare (a, b) { - var order = iteratee(a, b); - if (order !== 0) { - return order; - } else { - return stable.indexOf(a) - stable.indexOf(b); - } - }); - }, - /** - * @param {any} array - * @param {any} elem1 - * @param {any} elem2 - */ - swap: function (array, elem1, elem2) { - var i1 = array.indexOf(elem1); - var i2 = array.indexOf(elem2); - array[i2] = elem1; - array[i1] = elem2; - }, - - /** - * @param {string} value - * @param {boolean} allow_mailto - * @returns boolean - */ - is_email: function (value, allow_mailto) { - // http://stackoverflow.com/questions/46155/validate-email-address-in-javascript - var re; - if (allow_mailto) { - re = /^(mailto:)?(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; - } else { - re = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; - } - return re.test(value); - }, - - /** - * @param {any} str - * @param {any} elseValues - * @param {any} trueValues - * @param {any} falseValues - * @returns - */ - toBoolElse: function (str, elseValues, trueValues, falseValues) { - const ret = toBoolean(str, trueValues, falseValues); - if (typeof ret === "undefined") { - return elseValues; - } - return ret; - }, - /** - * @todo: is this really the correct place? - * - * @param {any} data - * @param {any} f - */ - traverse_records: function (data, f) { - if (data.type === 'record') { - f(data); - } else if (data.data) { - for (var i = 0; i < data.data.length; i++) { - utils.traverse_records(data.data[i], f); - } - } - }, - /** - * @param {any} node - * @param {any} strip_whitespace - * @returns - */ - xml_to_json: function (node, strip_whitespace) { - switch (node.nodeType) { - case 9: - return utils.xml_to_json(node.documentElement, strip_whitespace); - case 3: - case 4: - return (strip_whitespace && node.data.trim() === '') ? undefined : node.data; - case 1: - var attrs = $(node).getAttributes(); - return { - tag: node.tagName.toLowerCase(), - attrs: attrs, - children: Object.values(node.childNodes || {}) - .map((node) => { - return utils.xml_to_json(node, strip_whitespace); - }) - .filter((x) => !!x), - }; - } - }, - /** - * @param {any} node - * @returns {string} - */ - xml_to_str: function (node) { - var str = ""; - if (window.XMLSerializer) { - str = (new XMLSerializer()).serializeToString(node); - } else if (window.ActiveXObject) { - str = node.xml; - } else { - throw new Error(_t("Could not serialize XML")); - } - // Browsers won't deal with self closing tags except void elements: - // http://www.w3.org/TR/html-markup/syntax.html - var void_elements = 'area base br col command embed hr img input keygen link meta param source track wbr'.split(' '); - - // The following regex is a bit naive but it's ok for the xmlserializer output - str = str.replace(/<([a-z]+)([^<>]*)\s*\/\s*>/g, function (match, tag, attrs) { - if (void_elements.indexOf(tag) < 0) { - return "<" + tag + attrs + ">"; - } else { - return match; - } - }); - return str; - }, - /** - * Visit a tree of objects, where each children are in an attribute 'children'. - * For each children, we call the callback function given in arguments. - * - * @param {Object} tree an object describing a tree structure - * @param {function} f a callback - */ - traverse: function (tree, f) { - if (f(tree)) { - Object.values(tree.children || {}).forEach((c) => { - utils.traverse(c, f); - }); - } - }, - /** - * Enhanced traverse function with 'path' building on traverse. - * - * @param {Object} tree an object describing a tree structure - * @param {function} f a callback - * @param {Object} path the path to the current 'tree' object - */ - traversePath: function (tree, f, path) { - path = path || []; - f(tree, path); - Object.values(tree.children || {}).forEach((node) => { - utils.traversePath(node, f, path.concat(tree)); - }); - }, - /** - * Visit a tree of objects and freeze all - * - * @param {Object} obj - */ - deepFreeze: function (obj) { - var propNames = Object.getOwnPropertyNames(obj); - propNames.forEach(function(name) { - var prop = obj[name]; - if (typeof prop == 'object' && prop !== null) - utils.deepFreeze(prop); - }); - return Object.freeze(obj); - }, - - /** - * Find the closest value of the given one in the provided array - * - * @param {Number} num - * @param {Array} arr - * @returns {Number|undefined} - */ - closestNumber: function (num, arr) { - var curr = arr[0]; - var diff = Math.abs (num - curr); - for (var val = 0; val < arr.length; val++) { - var newdiff = Math.abs (num - arr[val]); - if (newdiff < diff) { - diff = newdiff; - curr = arr[val]; - } - } - return curr; - }, }; -export const confine = utils.confine -export const traverse = utils.traverse -export const is_email = utils.is_email -export const sprintf = utils.sprintf -export const groupBy = utils.groupBy -export const sortBy = utils.sortBy export default utils; diff --git a/addons/web/static/src/legacy/js/widgets/name_and_signature.js b/addons/web/static/src/legacy/js/widgets/name_and_signature.js index fffc132f65d..44d89ac0cde 100644 --- a/addons/web/static/src/legacy/js/widgets/name_and_signature.js +++ b/addons/web/static/src/legacy/js/widgets/name_and_signature.js @@ -1,12 +1,12 @@ /** @odoo-module **/ -import utils from "@web/legacy/js/core/utils"; import Widget from "@web/legacy/js/core/widget"; import { debounce } from "@web/core/utils/timing"; import { uniqueId } from "@web/core/utils/functions"; import { pick } from "@web/core/utils/objects"; import { renderToElement } from "@web/core/utils/render"; import { utils as uiUtils } from "@web/core/ui/ui_service"; +import { getDataURLFromFile } from "@web/core/utils/urls"; /** * This widget allows the user to input his name and to draw his signature. @@ -637,7 +637,7 @@ export var NameAndSignature = Widget.extend({ } this.$loadInvalid.addClass('d-none'); - utils.getDataURLFromFile(f).then(function (result) { + getDataURLFromFile(f).then(function (result) { self._printImage(result); }); }, diff --git a/addons/web/static/tests/core/utils/binary_tests.js b/addons/web/static/tests/core/utils/binary_tests.js new file mode 100644 index 00000000000..64ec4a3812f --- /dev/null +++ b/addons/web/static/tests/core/utils/binary_tests.js @@ -0,0 +1,14 @@ +/** @odoo-module **/ + +import { humanSize } from "@web/core/utils/binary"; + +QUnit.module("utils", () => { + QUnit.module("binary"); + + QUnit.test("humanSize", (assert) => { + assert.strictEqual(humanSize(0), "0.00 Bytes"); + assert.strictEqual(humanSize(3), "3.00 Bytes"); + assert.strictEqual(humanSize(2048), "2.00 Kb"); + assert.strictEqual(humanSize(2645000), "2.52 Mb"); + }); +}); diff --git a/addons/web/static/tests/core/utils/strings_tests.js b/addons/web/static/tests/core/utils/strings_tests.js index 2b3032d8c29..d5f62e48ade 100644 --- a/addons/web/static/tests/core/utils/strings_tests.js +++ b/addons/web/static/tests/core/utils/strings_tests.js @@ -4,6 +4,7 @@ import { escapeHTML, escapeRegExp, intersperse, + isEmail, sprintf, unaccent, unescapeHTML, @@ -134,4 +135,14 @@ QUnit.module("utils", () => { `Odoo` ); }); + + QUnit.test("isEmail", (assert) => { + assert.notOk(isEmail("")); + assert.notOk(isEmail("test")); + assert.notOk(isEmail("test@odoo")); + assert.notOk(isEmail("test@odoo@odoo.com")); + assert.notOk(isEmail("te st@odoo.com")); + + assert.ok(isEmail("test@odoo.com")); + }); }); diff --git a/addons/web/static/tests/legacy/core/util_tests.js b/addons/web/static/tests/legacy/core/util_tests.js deleted file mode 100644 index ab021109127..00000000000 --- a/addons/web/static/tests/legacy/core/util_tests.js +++ /dev/null @@ -1,305 +0,0 @@ -/** @odoo-module **/ - -import utils from "@web/legacy/js/core/utils"; -import { getDataURLFromFile } from "@web/core/utils/urls"; -import { localization } from "@web/core/l10n/localization"; -import { defaultLocalization } from "@web/../tests/helpers/mock_services"; -import { patchWithCleanup } from "@web/../tests/helpers/utils"; - -QUnit.module('core', {}, function () { - - QUnit.module('utils'); - - QUnit.test('findWhere', function (assert) { - assert.expect(7); - - const { findWhere } = utils; - - const list = [ - undefined, - { a: 1, b: 2 }, - { a: 2, b: 2 }, - { a: 1, b: 3 }, - { a: 1, b: 4 }, - { a: 2, b: 4 }, - ]; - - assert.deepEqual(findWhere(list, { a: 1 }), { a: 1, b: 2 }); - assert.deepEqual(findWhere(list, { a: 2 }), { a: 2, b: 2 }); - assert.deepEqual(findWhere(list, { b: 4 }), { a: 1, b: 4 }); - assert.deepEqual(findWhere(list, { b: 4, a: 2 }), { a: 2, b: 4 }); - assert.ok(findWhere([], { a: 1 }) === undefined); - assert.ok(findWhere(list, { a: 1, b: 5 }) === undefined); - assert.ok(findWhere(list, { c: 1 }) === undefined); - }); - - QUnit.test('groupBy', function (assert) { - assert.expect(7); - - const { groupBy } = utils; - - // Invalid - assert.throws( - () => groupBy({}), - new TypeError(`list is not iterable`) - ); - assert.throws( - () => groupBy([], true), - new Error(`Expected criterion of type 'string' or 'function' and got 'boolean'`) - ); - assert.throws( - () => groupBy([], 3), - new Error(`Expected criterion of type 'string' or 'function' and got 'number'`) - ); - assert.throws( - () => groupBy([], {}), - new Error(`Expected criterion of type 'string' or 'function' and got 'object'`) - ); - - // criterion = default - assert.deepEqual( - groupBy(["a", "b", 1, true]), - { - 1: [1], - a: ["a"], - b: ["b"], - true: [true], - } - ); - // criterion = string - assert.deepEqual( - groupBy([{ x: "a" }, { x: "a" }, { x: "b" }], "x"), - { - a: [{ x: "a" }, { x: "a" }], - b: [{ x: "b" }], - } - ); - // criterion = function - assert.deepEqual( - groupBy(["a", "b", 1, true], (x) => `el${x}`), - { - ela: ["a"], - elb: ["b"], - el1: [1], - eltrue: [true], - } - ); - }); - - QUnit.test('intersperse', function (assert) { - assert.expect(27); - - var intersperse = utils.intersperse; - - assert.strictEqual(intersperse("", []), ""); - assert.strictEqual(intersperse("0", []), "0"); - assert.strictEqual(intersperse("012", []), "012"); - assert.strictEqual(intersperse("1", []), "1"); - assert.strictEqual(intersperse("12", []), "12"); - assert.strictEqual(intersperse("123", []), "123"); - assert.strictEqual(intersperse("1234", []), "1234"); - assert.strictEqual(intersperse("123456789", []), "123456789"); - assert.strictEqual(intersperse("&ab%#@1", []), "&ab%#@1"); - - assert.strictEqual(intersperse("0", []), "0"); - assert.strictEqual(intersperse("0", [1]), "0"); - assert.strictEqual(intersperse("0", [2]), "0"); - assert.strictEqual(intersperse("0", [200]), "0"); - - assert.strictEqual(intersperse("12345678", [0], '.'), '12345678'); - assert.strictEqual(intersperse("", [1], '.'), ''); - assert.strictEqual(intersperse("12345678", [1], '.'), '1234567.8'); - assert.strictEqual(intersperse("12345678", [1], '.'), '1234567.8'); - assert.strictEqual(intersperse("12345678", [2], '.'), '123456.78'); - assert.strictEqual(intersperse("12345678", [2, 1], '.'), '12345.6.78'); - assert.strictEqual(intersperse("12345678", [2, 0], '.'), '12.34.56.78'); - assert.strictEqual(intersperse("12345678", [-1, 2], '.'), '12345678'); - assert.strictEqual(intersperse("12345678", [2, -1], '.'), '123456.78'); - assert.strictEqual(intersperse("12345678", [2, 0, 1], '.'), '12.34.56.78'); - assert.strictEqual(intersperse("12345678", [2, 0, 0], '.'), '12.34.56.78'); - assert.strictEqual(intersperse("12345678", [2, 0, -1], '.'), '12.34.56.78'); - assert.strictEqual(intersperse("12345678", [3,3,3,3], '.'), '12.345.678'); - assert.strictEqual(intersperse("12345678", [3,0], '.'), '12.345.678'); - }); - - QUnit.test('is_bin_size', function (assert) { - assert.expect(3); - - var is_bin_size = utils.is_bin_size; - - assert.strictEqual(is_bin_size('Cg=='), false); - assert.strictEqual(is_bin_size('2.5 Mb'), true); - // should also work for non-latin languages (e.g. russian) - assert.strictEqual(is_bin_size('64.2 Кб'), true); - }); - - QUnit.test('human_number', function (assert) { - assert.expect(26); - - patchWithCleanup(localization, { ...defaultLocalization }); - var human_number = utils.human_number; - - assert.strictEqual(human_number(1020, 2, 1), '1.02k'); - assert.strictEqual(human_number(1020000, 2, 2), '1020k'); - assert.strictEqual(human_number(10200000, 2, 2), '10.2M'); - assert.strictEqual(human_number(1020, 2, 1), '1.02k'); - assert.strictEqual(human_number(1002, 2, 1), '1k'); - assert.strictEqual(human_number(101, 2, 1), '101'); - assert.strictEqual(human_number(64.2, 2, 1), '64'); - assert.strictEqual(human_number(1e+18), '1E'); - assert.strictEqual(human_number(1e+21, 2, 1), '1e+21'); - assert.strictEqual(human_number(1.0045e+22, 2, 1), '1e+22'); - assert.strictEqual(human_number(1.0045e+22, 3, 1), '1.005e+22'); - assert.strictEqual(human_number(1.012e+43, 2, 1), '1.01e+43'); - assert.strictEqual(human_number(1.012e+43, 2, 2), '1.01e+43'); - - assert.strictEqual(human_number(-1020, 2, 1), '-1.02k'); - assert.strictEqual(human_number(-1020000, 2, 2), '-1020k'); - assert.strictEqual(human_number(-10200000, 2, 2), '-10.2M'); - assert.strictEqual(human_number(-1020, 2, 1), '-1.02k'); - assert.strictEqual(human_number(-1002, 2, 1), '-1k'); - assert.strictEqual(human_number(-101, 2, 1), '-101'); - assert.strictEqual(human_number(-64.2, 2, 1), '-64'); - assert.strictEqual(human_number(-1e+18), '-1E'); - assert.strictEqual(human_number(-1e+21, 2, 1), '-1e+21'); - assert.strictEqual(human_number(-1.0045e+22, 2, 1), '-1e+22'); - assert.strictEqual(human_number(-1.0045e+22, 3, 1), '-1.004e+22'); - assert.strictEqual(human_number(-1.012e+43, 2, 1), '-1.01e+43'); - assert.strictEqual(human_number(-1.012e+43, 2, 2), '-1.01e+43'); - }); - - QUnit.test('round_decimals', function (assert) { - assert.expect(21); - - var round_di = utils.round_decimals; - - assert.strictEqual(String(round_di(1.0, 0)), '1'); - assert.strictEqual(String(round_di(1.0, 1)), '1'); - assert.strictEqual(String(round_di(1.0, 2)), '1'); - assert.strictEqual(String(round_di(1.0, 3)), '1'); - assert.strictEqual(String(round_di(1.0, 4)), '1'); - assert.strictEqual(String(round_di(1.0, 5)), '1'); - assert.strictEqual(String(round_di(1.0, 6)), '1'); - assert.strictEqual(String(round_di(1.0, 7)), '1'); - assert.strictEqual(String(round_di(1.0, 8)), '1'); - assert.strictEqual(String(round_di(0.5, 0)), '1'); - assert.strictEqual(String(round_di(-0.5, 0)), '-1'); - assert.strictEqual(String(round_di(2.6745, 3)), '2.6750000000000003'); - assert.strictEqual(String(round_di(-2.6745, 3)), '-2.6750000000000003'); - assert.strictEqual(String(round_di(2.6744, 3)), '2.674'); - assert.strictEqual(String(round_di(-2.6744, 3)), '-2.674'); - assert.strictEqual(String(round_di(0.0004, 3)), '0'); - assert.strictEqual(String(round_di(-0.0004, 3)), '0'); - assert.strictEqual(String(round_di(357.4555, 3)), '357.456'); - assert.strictEqual(String(round_di(-357.4555, 3)), '-357.456'); - assert.strictEqual(String(round_di(457.4554, 3)), '457.455'); - assert.strictEqual(String(round_di(-457.4554, 3)), '-457.455'); - }); - - QUnit.test('round_precision', function (assert) { - assert.expect(26); - - var round_pr = utils.round_precision; - - assert.strictEqual(String(round_pr(1.0, 1)), '1'); - assert.strictEqual(String(round_pr(1.0, 0.1)), '1'); - assert.strictEqual(String(round_pr(1.0, 0.01)), '1'); - assert.strictEqual(String(round_pr(1.0, 0.001)), '1'); - assert.strictEqual(String(round_pr(1.0, 0.0001)), '1'); - assert.strictEqual(String(round_pr(1.0, 0.00001)), '1'); - assert.strictEqual(String(round_pr(1.0, 0.000001)), '1'); - assert.strictEqual(String(round_pr(1.0, 0.0000001)), '1'); - assert.strictEqual(String(round_pr(1.0, 0.00000001)), '1'); - assert.strictEqual(String(round_pr(0.5, 1)), '1'); - assert.strictEqual(String(round_pr(-0.5, 1)), '-1'); - assert.strictEqual(String(round_pr(2.6745, 0.001)), '2.6750000000000003'); - assert.strictEqual(String(round_pr(-2.6745, 0.001)), '-2.6750000000000003'); - assert.strictEqual(String(round_pr(2.6744, 0.001)), '2.674'); - assert.strictEqual(String(round_pr(-2.6744, 0.001)), '-2.674'); - assert.strictEqual(String(round_pr(0.0004, 0.001)), '0'); - assert.strictEqual(String(round_pr(-0.0004, 0.001)), '0'); - assert.strictEqual(String(round_pr(357.4555, 0.001)), '357.456'); - assert.strictEqual(String(round_pr(-357.4555, 0.001)), '-357.456'); - assert.strictEqual(String(round_pr(457.4554, 0.001)), '457.455'); - assert.strictEqual(String(round_pr(-457.4554, 0.001)), '-457.455'); - assert.strictEqual(String(round_pr(-457.4554, 0.05)), '-457.45000000000005'); - assert.strictEqual(String(round_pr(457.444, 0.5)), '457.5'); - assert.strictEqual(String(round_pr(457.3, 5)), '455'); - assert.strictEqual(String(round_pr(457.5, 5)), '460'); - assert.strictEqual(String(round_pr(457.1, 3)), '456'); - }); - - QUnit.test('sortBy', function (assert) { - assert.expect(29); - const { sortBy } = utils; - const bools = [true, false, true]; - const ints = [2, 1, 5]; - const strs = ['b', 'a', 'z']; - const objbools = [{ x: true }, { x: false }, { x: true }]; - const objints = [{ x: 2 }, { x: 1 }, { x: 5 }]; - const objstrss = [{ x: 'b' }, { x: 'a' }, { x: 'z' }]; - - // Invalid - assert.throws( - () => sortBy({}), - new TypeError(`array.slice is not a function`) - ); - assert.throws( - () => sortBy([Symbol('b'), Symbol('a')]), - TypeError - ); - assert.throws( - () => sortBy(ints, true), - new Error(`Expected criterion of type 'string' or 'function' and got 'boolean'`) - ); - assert.throws( - () => sortBy(ints, 3), - new Error(`Expected criterion of type 'string' or 'function' and got 'number'`) - ); - assert.throws( - () => sortBy(ints, {}), - new Error(`Expected criterion of type 'string' or 'function' and got 'object'`) - ); - // Do not sort in place - const toSort = [2, 3, 1]; - sortBy(toSort); - assert.deepEqual(toSort, [2, 3, 1]); - // Sort (no criterion) - assert.deepEqual(sortBy([]), []); - assert.deepEqual(sortBy(ints), [1, 2, 5]); - assert.deepEqual(sortBy(bools), [false, true, true]); - assert.deepEqual(sortBy(strs), ['a', 'b', 'z']); - assert.deepEqual(sortBy(objbools), [{ x: true }, { x: false }, { x: true }]); - assert.deepEqual(sortBy(objints), [{ x: 2 }, { x: 1 }, { x: 5 }]); - assert.deepEqual(sortBy(objstrss), [{ x: 'b' }, { x: 'a' }, { x: 'z' }]); - // Sort by property - const prop = 'x'; - assert.deepEqual(sortBy([], prop), []); - assert.deepEqual(sortBy(ints, prop), [2, 1, 5]); - assert.deepEqual(sortBy(bools, prop), [true, false, true]); - assert.deepEqual(sortBy(strs, prop), ['b', 'a', 'z']); - assert.deepEqual(sortBy(objbools, prop), [{ x: false }, { x: true }, { x: true }]); - assert.deepEqual(sortBy(objints, prop), [{ x: 1 }, { x: 2 }, { x: 5 }]); - assert.deepEqual(sortBy(objstrss, prop), [{ x: 'a' }, { x: 'b' }, { x: 'z' }]); - // Sort by getter - const getter = obj => obj.x; - assert.deepEqual(sortBy([], getter), []); - assert.deepEqual(sortBy(ints, getter), [2, 1, 5]); - assert.deepEqual(sortBy(bools, getter), [true, false, true]); - assert.deepEqual(sortBy(strs, getter), ['b', 'a', 'z']); - assert.deepEqual(sortBy(objbools, getter), [{ x: false }, { x: true }, { x: true }]); - assert.deepEqual(sortBy(objints, getter), [{ x: 1 }, { x: 2 }, { x: 5 }]); - assert.deepEqual(sortBy(objstrss, getter), [{ x: 'a' }, { x: 'b' }, { x: 'z' }]); - // Descending order - assert.deepEqual(sortBy(ints, null, 'desc'), [5, 2, 1]); - assert.deepEqual(sortBy(objstrss, prop, 'desc'), [{ x: 'z' }, { x: 'b' }, { x: 'a' }]); - }); - - QUnit.test('getDataURLFromFile handles empty file', async function (assert) { - assert.expect(1); - - const emptyFile = new File([""], "empty.txt", { type: "text/plain" }); - const dataUrl = await getDataURLFromFile(emptyFile); - assert.strictEqual(dataUrl, "data:text/plain;base64,", "dataURL for empty file is not proper"); - }); -}); diff --git a/addons/web_editor/static/src/components/upload_progress_toast/upload_service.js b/addons/web_editor/static/src/components/upload_progress_toast/upload_service.js index 5dcf3dc82cd..4baa4824ee9 100644 --- a/addons/web_editor/static/src/components/upload_progress_toast/upload_service.js +++ b/addons/web_editor/static/src/components/upload_progress_toast/upload_service.js @@ -2,9 +2,9 @@ import { registry } from '@web/core/registry'; import { UploadProgressToast } from './upload_progress_toast'; -import utils from '@web/legacy/js/core/utils'; import { checkFileSize } from "@web/core/utils/files"; import { humanNumber } from "@web/core/utils/numbers"; +import { getDataURLFromFile } from "@web/core/utils/urls"; import { reactive } from "@odoo/owl"; export const AUTOCLOSE_DELAY = 3000; @@ -94,7 +94,7 @@ export const uploadService = { // limited by bandwidth. for (const sortedFile of sortedFiles) { const file = progressToast.files[sortedFile.progressToastId]; - const dataURL = await utils.getDataURLFromFile(sortedFile); + const dataURL = await getDataURLFromFile(sortedFile); try { const xhr = new XMLHttpRequest(); xhr.upload.addEventListener('progress', ev => { diff --git a/addons/web_editor/static/src/js/editor/snippets.editor.js b/addons/web_editor/static/src/js/editor/snippets.editor.js index cb3db4d679e..952a179006a 100644 --- a/addons/web_editor/static/src/js/editor/snippets.editor.js +++ b/addons/web_editor/static/src/js/editor/snippets.editor.js @@ -1,10 +1,11 @@ /** @odoo-module **/ import { Mutex } from "@web/core/utils/concurrency"; +import { clamp } from "@web/core/utils/numbers"; import core from "@web/legacy/js/services/core"; import Dialog from "@web/legacy/js/core/dialog"; import dom from "@web/legacy/js/core/dom"; -import { Markup, confine } from "@web/legacy/js/core/utils"; +import { Markup } from "@web/legacy/js/core/utils"; import Widget from "@web/legacy/js/core/widget"; import options from "@web_editor/js/editor/snippets.options"; import { SmoothScrollOnDrag } from "@web_editor/js/editor/smooth_scroll_on_drag"; @@ -1609,7 +1610,7 @@ var SnippetEditor = Widget.extend({ let left = ev.pageX - rowElLeft - columnMiddle; // Horizontal overflow. - left = confine(left, 0, rowEl.clientWidth - columnWidth); + left = clamp(left, 0, rowEl.clientWidth - columnWidth); columnEl.style.top = top + 'px'; columnEl.style.left = left + 'px'; diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js index b2fbdbe8316..c35f536119d 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -6,7 +6,7 @@ import Dialog from "@web/legacy/js/core/dialog"; import dom from "@web/legacy/js/core/dom"; import rpc from "@web/legacy/js/core/rpc"; import { throttleForAnimation, debounce } from "@web/core/utils/timing"; -import utils from "@web/legacy/js/core/utils"; +import { clamp } from "@web/core/utils/numbers"; import Widget from "@web/legacy/js/core/widget"; import { ColorPalette } from "@web_editor/js/wysiwyg/widgets/color_palette"; import weUtils from "@web_editor/js/common/utils"; @@ -6498,7 +6498,7 @@ registry.ImageTools = ImageHandlerOption.extend({ } else if (img.closest('.container, .o_container_small')) { const mdContainerMaxWidth = parseFloat(computedStyles.getPropertyValue('--o-md-container-max-width')) || 720; const mdContainerInnerWidth = mdContainerMaxWidth - gutterWidth; - return Math.round(utils.confine(displayWidth, mdContainerInnerWidth, this.MAX_SUGGESTED_WIDTH)); + return Math.round(clamp(displayWidth, mdContainerInnerWidth, this.MAX_SUGGESTED_WIDTH)); // If the image is displayed in a container-fluid, it might also get // bigger on smaller screens. The same way, we suggest the width of the // current image unless it is smaller than the max size of the container @@ -6507,7 +6507,7 @@ registry.ImageTools = ImageHandlerOption.extend({ } else if (img.closest('.container-fluid')) { const lgBp = parseFloat(computedStyles.getPropertyValue('--breakpoint-lg')) || 992; const mdContainerFluidMaxInnerWidth = lgBp - gutterWidth; - return Math.round(utils.confine(displayWidth, mdContainerFluidMaxInnerWidth, this.MAX_SUGGESTED_WIDTH)); + return Math.round(clamp(displayWidth, mdContainerFluidMaxInnerWidth, this.MAX_SUGGESTED_WIDTH)); } // If it's not in a container, it's probably not going to change size // depending on breakpoints. We still keep a margin safety. diff --git a/addons/website/static/src/client_actions/configurator/configurator.js b/addons/website/static/src/client_actions/configurator/configurator.js index 0955a57746d..b102018c3a3 100644 --- a/addons/website/static/src/client_actions/configurator/configurator.js +++ b/addons/website/static/src/client_actions/configurator/configurator.js @@ -1,7 +1,7 @@ /** @odoo-module **/ import { delay } from "@web/core/utils/concurrency"; -import utils from '@web/legacy/js/core/utils'; +import { getDataURLFromFile } from "@web/core/utils/urls"; import weUtils from '@web_editor/js/common/utils'; import { _t } from "@web/core/l10n/translation"; import {svgToPNG} from '@website/js/utils'; @@ -267,7 +267,7 @@ class PaletteSelectionScreen extends Component { const logoSelectInput = this.logoInputRef.el; if (logoSelectInput.files.length === 1) { const file = logoSelectInput.files[0]; - const data = await utils.getDataURLFromFile(file); + const data = await getDataURLFromFile(file); const attachment = await this.rpc('/web_editor/attachment/add_data', { 'name': 'logo', 'data': data.split(',')[1], diff --git a/addons/website/static/src/snippets/s_facebook_page/000.js b/addons/website/static/src/snippets/s_facebook_page/000.js index cc9e39d5836..0dd5706215f 100644 --- a/addons/website/static/src/snippets/s_facebook_page/000.js +++ b/addons/website/static/src/snippets/s_facebook_page/000.js @@ -1,8 +1,8 @@ /** @odoo-module **/ import { pick } from "@web/core/utils/objects"; +import { clamp } from "@web/core/utils/numbers"; import publicWidget from "@web/legacy/js/public/public_widget"; -import utils from "@web/legacy/js/core/utils"; const FacebookPageWidget = publicWidget.Widget.extend({ selector: '.o_facebook_page', @@ -24,7 +24,7 @@ const FacebookPageWidget = publicWidget.Widget.extend({ params.href = `https://www.facebook.com/${params.id}`; } delete params.id; - params.width = utils.confine(Math.floor(this.$el.width()), 180, 500); + params.width = clamp(Math.floor(this.$el.width()), 180, 500); var src = $.param.querystring('https://www.facebook.com/plugins/page.php', params); this.$iframe = $('