[REF] web,*: stop using and remove legacy utils
*account,web_editor,website,website_sale,website_slides This commit removes all legacy utils except Markup, which will be removed in another PR. A lot of those utils were no longer used and thus have just been removed. Those that were still used either already existed in the wowl codebase, so usecases have been adapted to use the new version instead. A few have been re-implemented (or moved basically) to the wowl codebase (e.g. isEmail and humanSize). Part of task~3439226 closes odoo/odoo#134620 Related: odoo/enterprise#47044 Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()}`;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Number>} indices
|
||||
* @param {String} separator
|
||||
* @returns {String}
|
||||
*/
|
||||
intersperse: function (str, indices, separator) {
|
||||
separator = separator || '';
|
||||
var result = [], last = str.length;
|
||||
|
||||
for(var i=0; i<indices.length; ++i) {
|
||||
var section = indices[i];
|
||||
if (section === -1 || last <= 0) {
|
||||
// Done with string, or -1 (stops formatting string)
|
||||
break;
|
||||
} else if(section === 0 && i === 0) {
|
||||
// repeats previous section, which there is none => 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, '>').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, '>').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 + '</' + node.tag + '>';
|
||||
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 + "></" + tag + ">";
|
||||
} 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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
escapeHTML,
|
||||
escapeRegExp,
|
||||
intersperse,
|
||||
isEmail,
|
||||
sprintf,
|
||||
unaccent,
|
||||
unescapeHTML,
|
||||
@@ -134,4 +135,14 @@ QUnit.module("utils", () => {
|
||||
`<a href="https://www.odoo.com">Odoo<a>`
|
||||
);
|
||||
});
|
||||
|
||||
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"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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 = $('<iframe/>', {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import utils from "@web/legacy/js/core/utils";
|
||||
import { clamp } from "@web/core/utils/numbers";
|
||||
import options from "@web_editor/js/editor/snippets.options";
|
||||
|
||||
options.registry.progress = options.Class.extend({
|
||||
@@ -43,7 +43,7 @@ options.registry.progress = options.Class.extend({
|
||||
*/
|
||||
progressBarValue: function (previewMode, widgetValue, params) {
|
||||
let value = parseInt(widgetValue);
|
||||
value = utils.confine(value, 0, 100);
|
||||
value = clamp(value, 0, 100);
|
||||
const $progressBar = this.$target.find('.progress-bar');
|
||||
const $progressBarText = this.$target.find('.s_progress_bar_text');
|
||||
// Target precisely the XX% not only XX to not replace wrong element
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { KeepLast } from "@web/core/utils/concurrency";
|
||||
import utils from "@web/legacy/js/core/utils";
|
||||
import ajax from "@web/legacy/js/core/ajax";
|
||||
import { memoize, uniqueId } from "@web/core/utils/functions";
|
||||
import { throttleForAnimation } from "@web/core/utils/timing";
|
||||
import { insertThousandsSep } from "@web/core/utils/numbers";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
|
||||
@@ -620,8 +620,9 @@ var VariantMixin = {
|
||||
precision = parseInt($('.decimal_precision').last().data('precision'));
|
||||
}
|
||||
var formatted = price.toFixed(precision).split(".");
|
||||
formatted[0] = utils.insert_thousand_seps(formatted[0]);
|
||||
return formatted.join(localization.decimalPoint);
|
||||
const { thousandsSep, decimalPoint, grouping } = localization;
|
||||
formatted[0] = insertThousandsSep(formatted[0], thousandsSep, grouping);
|
||||
return formatted.join(decimalPoint);
|
||||
},
|
||||
/**
|
||||
* Returns a throttled `_getCombinationInfo` with a leading and a trailing
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { WebsiteSale } from '@website_sale/js/website_sale';
|
||||
import { is_email } from '@web/legacy/js/core/utils';
|
||||
import { isEmail } from '@web/core/utils/strings';
|
||||
|
||||
WebsiteSale.include({
|
||||
events: Object.assign({}, WebsiteSale.prototype.events, {
|
||||
@@ -34,7 +34,7 @@ WebsiteSale.include({
|
||||
const formEl = stockNotificationEl.querySelector('#stock_notification_form');
|
||||
const email = stockNotificationEl.querySelector('#stock_notification_input').value.trim();
|
||||
|
||||
if (!is_email(email)) {
|
||||
if (!isEmail(email)) {
|
||||
return this._displayEmailIncorrectMessage(stockNotificationEl);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
import { uniqueId } from '@web/core/utils/functions';
|
||||
import { renderToElement } from "@web/core/utils/render";
|
||||
import { getDataURLFromFile } from "@web/core/utils/urls";
|
||||
import Dialog from '@web/legacy/js/core/dialog';
|
||||
import publicWidget from '@web/legacy/js/public/public_widget';
|
||||
import utils from '@web/legacy/js/core/utils';
|
||||
import wUtils from '@website/js/utils';
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
@@ -577,7 +577,7 @@ var SlideUploadDialog = Dialog.extend({
|
||||
|
||||
const preview = document.getElementById("slide-image");
|
||||
if (file.type !== 'application/pdf') {
|
||||
utils.getDataURLFromFile(file).then(dataURL => {
|
||||
getDataURLFromFile(file).then(dataURL => {
|
||||
if (isImage) {
|
||||
preview.src = dataURL;
|
||||
}
|
||||
@@ -586,7 +586,7 @@ var SlideUploadDialog = Dialog.extend({
|
||||
});
|
||||
} else {
|
||||
this.set('can_submit_form', false);
|
||||
utils.getDataURLFromFile(file).then(async dataURL => {
|
||||
getDataURLFromFile(file).then(async dataURL => {
|
||||
this.file.data = dataURL.split(',', 2)[1];
|
||||
/**
|
||||
* The following line fixes pdfjsLib 'Util' global variable.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import utils from '@web/legacy/js/core/utils';
|
||||
import { getDataURLFromFile } from "@web/core/utils/urls";
|
||||
|
||||
/*
|
||||
* Constant
|
||||
@@ -107,7 +107,7 @@ const fillInFileInput = async (input, name, type, content) => {
|
||||
const compareBase64Content = async (url, name, type, expectedContent) => {
|
||||
const blob = await (await fetch(url)).blob();
|
||||
const file = new File([blob], name, { type: type});
|
||||
const actualContent = await utils.getDataURLFromFile(file).then(dataURL => dataURL.split(',', 2)[1]);
|
||||
const actualContent = await getDataURLFromFile(file).then(dataURL => dataURL.split(',', 2)[1]);
|
||||
return expectedContent === actualContent;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user