From 1fced414ab362da994cf8b3fbb045cbba35301f3 Mon Sep 17 00:00:00 2001 From: Fabre Florian Date: Tue, 14 Jan 2025 15:36:10 +0100 Subject: [PATCH 01/29] ArrayTools & GeneratorTools are no longer used (only by jqplot). So drop them and use underscore or d3 instead. --- CHANGELOG.txt | 2 + .../creme_core/js/lib/generators-0.1.js | 181 ---------------- .../static/creme_core/js/tests/generators.js | 204 ------------------ creme/settings.py | 2 - 4 files changed, 2 insertions(+), 387 deletions(-) delete mode 100644 creme/creme_core/static/creme_core/js/lib/generators-0.1.js delete mode 100644 creme/creme_core/static/creme_core/js/tests/generators.js diff --git a/CHANGELOG.txt b/CHANGELOG.txt index aa46d098fa..a26db78aeb 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -213,6 +213,8 @@ - "creme_core-hatmenubar-form" - "creme_core-hatmenubar-update" - "creme_core-hatmenubar-view" + - Javascript : + * ArrayTools & GeneratorTools are no longer used (only by jqplot). So drop them and use underscore or d3 instead. - Apps : * Activities: - Refactor creme.ActivityCalendar : can be now used as independent component. diff --git a/creme/creme_core/static/creme_core/js/lib/generators-0.1.js b/creme/creme_core/static/creme_core/js/lib/generators-0.1.js deleted file mode 100644 index a49c47bd68..0000000000 --- a/creme/creme_core/static/creme_core/js/lib/generators-0.1.js +++ /dev/null @@ -1,181 +0,0 @@ -/******************************************************************************* - * Creme is a free/open-source Customer Relationship Management software - * Copyright (C) 2009-2012 Hybird - * - * This program is free software: you can redistribute it and/or modify it under - * the terms of the GNU Affero General Public License as published by the Free - * Software Foundation, either version 3 of the License, or (at your option) any - * later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more - * details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - ******************************************************************************/ - -(function() { -"use strict"; - -window.ArrayTools = { - get: function(data, index, default_value) { - var value = null; - - if (index === -1 || index === (data.length - 1)) { - value = data.slice(index); - } else { - value = data.slice(index, index + 1); - } - - return value.length ? value[0] : default_value; - }, - - set: function(data, index, value) { - if (index >= 0) { - data[index] = value; - return data; - } - - if (index >= -data.length) { - data.splice(index, 1, value); - return data; - } - - var prev = []; - prev[-(data.length + index + 1)] = value; - - Array.prototype.unshift.apply(data, prev.reverse()); - return data; - }, - - remove: function(data, index) { - return data.splice(index, 1)[0]; - }, - - sum: function(data, start, end) { - var total = 0.0; - data = (start !== undefined) ? data.slice(start, end) : data; - - data.forEach(function(value) { - total += window.isNaN(value) ? 0.0 : value; - }); - return total; - }, - - swap: function(data, prev, next) { - var next_val = this.get(data, next); - var prev_val = this.get(data, prev); - - this.set(data, next, prev_val); - this.set(data, prev, next_val); - - return data; - } - -/* TODO : never used ? - insertOrReplace: function(data, replaceIndex, insertIndex, value) { - if (insertIndex !== undefined) { - data.splice(insertIndex, 0, value); - } else { - ArrayTools.set(data, replaceIndex, value); - } - - return data; - } -*/ -}; - -window.Generator = function() { - this._getter = undefined; - this._processor = undefined; -}; - -Generator.prototype = { - _next: function(entry, index, data) { - var value = this._getter ? this._getter.bind(this)(entry, index, data) : entry; - - if (value !== undefined && this._processor) { - value = this._processor.bind(this)(value, index, data); - } - - return value; - }, - - get: function(getter) { - if (getter === undefined) { - return this._getter; - } - - if (typeof getter === 'number') { - var _index = getter; - this._getter = function(entry, index, data) { - return ArrayTools.get(entry, _index); - }; - } else if (Object.isFunc(getter)) { - this._getter = getter; - } else if (getter !== null) { - var _key = getter; - this._getter = function(entry, index, data) { - return entry[_key]; - }; - } else { - this._getter = undefined; - } - - return this; - }, - - each: function(processor) { - if (processor === undefined) { - return this._processor; - } - - this._processor = Object.isFunc(processor) ? processor : undefined; - return this; - }, - - iterator: function() { - var self = this; - - return function(element, index, array) { - return self._next(element, index, array); - }; - } -}; - -window.GeneratorTools = { - array: { - swap: function(prev, next) { - return function(value, index, data) { - return ArrayTools.swap(value.slice(), prev, next); - }; - }, - - ratio: function(valueIndex, total, ratio, targetIndex) { - return function(value, index, data) { - var val = (ArrayTools.get(value, valueIndex, 0.0) * ratio) / total; - var array = value.slice(); - - if (targetIndex !== undefined) { - array.splice(targetIndex, 0, val); - } else { - ArrayTools.set(array, valueIndex, val); - } - - return array; - }; - }, - - format: function(format, targetIndex) { - return function(value, index, data) { - var array = value.slice(); - array.splice(targetIndex !== undefined ? targetIndex : value.length, 0, format.format(value)); - - return array; - }; - } - } -}; -}()); diff --git a/creme/creme_core/static/creme_core/js/tests/generators.js b/creme/creme_core/static/creme_core/js/tests/generators.js deleted file mode 100644 index 0aae3fdd1c..0000000000 --- a/creme/creme_core/static/creme_core/js/tests/generators.js +++ /dev/null @@ -1,204 +0,0 @@ -(function($) { -QUnit.module("creme.generators.js", new QUnitMixin()); - -QUnit.test('generators.ArrayTools.get', function(assert) { - equal(ArrayTools.get([12, 3, 8, 5, 44], 0), 12); - equal(ArrayTools.get([12, 3, 8, 5, 44], 3), 5); - equal(ArrayTools.get([12, 3, 8, 5, 44], 4), 44); - - equal(ArrayTools.get([12, 3, 8, 5, 44], -1), 44); - equal(ArrayTools.get([12, 3, 8, 5, 44], -2), 5); - equal(ArrayTools.get([12, 3, 8, 5, 44], -5), 12); - - equal(ArrayTools.get([12, 3, 8, 5, 44], 5), undefined); - equal(ArrayTools.get([12, 3, 8, 5, 44], -6), undefined); - - equal(ArrayTools.get([12, 3, 8, 5, 44], 5, 404), 404); - equal(ArrayTools.get([12, 3, 8, 5, 44], -6, 404), 404); -}); - -QUnit.test('generators.ArrayTools.set', function(assert) { - deepEqual(ArrayTools.set([12, 3, 8, 5, 44], 0, "a"), ["a", 3, 8, 5, 44]); - deepEqual(ArrayTools.set([12, 3, 8, 5, 44], 3, "a"), [12, 3, 8, "a", 44]); - deepEqual(ArrayTools.set([12, 3, 8, 5, 44], 4, "a"), [12, 3, 8, 5, "a"]); - - deepEqual(ArrayTools.set([12, 3, 8, 5, 44], -1, "a"), [12, 3, 8, 5, "a"]); - deepEqual(ArrayTools.set([12, 3, 8, 5, 44], -2, "a"), [12, 3, 8, "a", 44]); - deepEqual(ArrayTools.set([12, 3, 8, 5, 44], -5, "a"), ["a", 3, 8, 5, 44]); - - deepEqual(ArrayTools.set([12, 3, 8, 5, 44], 5, "a"), [12, 3, 8, 5, 44, "a"]); - deepEqual(ArrayTools.set([12, 3, 8, 5, 44], -6, "a"), ["a", 12, 3, 8, 5, 44]); - - deepEqual(ArrayTools.set([12, 3, 8, 5, 44], 9, "a"), [12, 3, 8, 5, 44, undefined, undefined, undefined, undefined, "a"]); - deepEqual(ArrayTools.set([12, 3, 8, 5, 44], -10, "a"), ["a", undefined, undefined, undefined, undefined, 12, 3, 8, 5, 44]); -}); - -QUnit.test('generators.ArrayTools.remove', function(assert) { - var array = [12, 3, 8, 5, 44]; - equal(ArrayTools.remove(array, 0), 12); - deepEqual(array, [3, 8, 5, 44]); - - array = [12, 3, 8, 5, 44]; - equal(ArrayTools.remove(array, 3), 5); - deepEqual(array, [12, 3, 8, 44]); - - array = [12, 3, 8, 5, 44]; - equal(ArrayTools.remove(array, 4), 44); - deepEqual(array, [12, 3, 8, 5]); - - array = [12, 3, 8, 5, 44]; - equal(ArrayTools.remove(array, -1), 44); - deepEqual(array, [12, 3, 8, 5]); - - array = [12, 3, 8, 5, 44]; - equal(ArrayTools.remove(array, -2), 5); - deepEqual(array, [12, 3, 8, 44]); - - array = [12, 3, 8, 5, 44]; - equal(ArrayTools.remove(array, -5), 12); - deepEqual(array, [3, 8, 5, 44]); -}); - -QUnit.test('generators.ArrayTools.sum', function(assert) { - equal(0, ArrayTools.sum([])); - - equal(10, ArrayTools.sum([1, 2, 3, 4])); - equal(3 + 4, ArrayTools.sum([1, 2, 3, 4], 2)); - equal(1 + 2, ArrayTools.sum([1, 2, 3, 4], 0, 2)); - equal(1 + 2 + 3, ArrayTools.sum([1, 2, 3, 4], 0, 3)); - - equal(7, ArrayTools.sum([1, 2, undefined, 4])); - equal(7, ArrayTools.sum([1, 2, "a", 4])); -}); - -QUnit.test('generators.ArrayTools.swap', function(assert) { - // same index - deepEqual(ArrayTools.swap([12, 3, 8, 5, 44], 0, 0), [12, 3, 8, 5, 44]); - deepEqual(ArrayTools.swap([12, 3, 8, 5, 44], 2, 2), [12, 3, 8, 5, 44]); - deepEqual(ArrayTools.swap([12, 3, 8, 5, 44], 4, 4), [12, 3, 8, 5, 44]); - - deepEqual(ArrayTools.swap([12, 3, 8, 5, 44], -1, -1), [12, 3, 8, 5, 44]); - deepEqual(ArrayTools.swap([12, 3, 8, 5, 44], -5, -5), [12, 3, 8, 5, 44]); - - // same relative index - deepEqual(ArrayTools.swap([12, 3, 8, 5, 44], 0, -5), [12, 3, 8, 5, 44]); - deepEqual(ArrayTools.swap([12, 3, 8, 5, 44], -5, 0), [12, 3, 8, 5, 44]); - - deepEqual(ArrayTools.swap([12, 3, 8, 5, 44], 3, -2), [12, 3, 8, 5, 44]); - deepEqual(ArrayTools.swap([12, 3, 8, 5, 44], -2, 3), [12, 3, 8, 5, 44]); - - // swap - deepEqual(ArrayTools.swap([12, 3, 8, 5, 44], 0, 1), [3, 12, 8, 5, 44]); - deepEqual(ArrayTools.swap([12, 3, 8, 5, 44], 0, 3), [5, 3, 8, 12, 44]); - deepEqual(ArrayTools.swap([12, 3, 8, 5, 44], 0, 4), [44, 3, 8, 5, 12]); - - // swap relative - deepEqual(ArrayTools.swap([12, 3, 8, 5, 44], 0, -1), [44, 3, 8, 5, 12]); - deepEqual(ArrayTools.swap([12, 3, 8, 5, 44], 0, -2), [5, 3, 8, 12, 44]); - - deepEqual(ArrayTools.swap([12, 3, 8, 5, 44], -1, 0), [44, 3, 8, 5, 12]); - deepEqual(ArrayTools.swap([12, 3, 8, 5, 44], -2, 0), [5, 3, 8, 12, 44]); -}); - -QUnit.test('generators.Generator.get (number)', function(assert) { - var g = new Generator(); - equal(undefined, g.get()); - - equal(g, g.get(2)); - equal(true, Object.isFunc(g.get())); - equal(8, g.get()([12, 3, 8])); - equal(undefined, g.get()([12, 3])); - equal(8, g.get()([12, 3, 8, 15])); -}); - -QUnit.test('generators.Generator.get (function)', function(assert) { - var g = new Generator(); - - equal(g, g.get(function() { return 'test !'; })); - equal(true, Object.isFunc(g.get())); - equal('test !', g.get()()); -}); - -QUnit.test('generators.Generator.get (object)', function(assert) { - var g = new Generator(); - - equal(g, g.get('a')); - equal(true, Object.isFunc(g.get())); - - equal(12, g.get()({a: 12, b: -5})); - equal(-5, g.get()({a: -5, b: 12})); - equal(undefined, g.get()({b: 12})); -}); - -QUnit.test('generators.Generator.get (null)', function(assert) { - var g = new Generator().get('a'); - equal(true, Object.isFunc(g.get())); - - g.get(null); - equal(undefined, g.get()); -}); - -QUnit.test('generators.Generator.each (aka processor)', function(assert) { - var g = new Generator(); - equal(undefined, g.each()); - - g.each(function(value, index, data) { return value; }); - equal(true, Object.isFunc(g.each())); -}); - -QUnit.test('generators.Generator.each (null)', function(assert) { - var g = new Generator().each(function(value, index, data) { return value; }); - equal(true, Object.isFunc(g.each())); - - g.each(null); - equal(undefined, g.each()); -}); - -QUnit.test('generators.Generator.iterator', function(assert) { - var g = new Generator(); - var iter = g.iterator(); - - equal(true, Object.isFunc(iter)); - - deepEqual([], [].map(iter)); - deepEqual([1, 2, 3], [1, 2, 3].map(iter)); -}); - -QUnit.test('generators.Generator.iterator (with processor)', function(assert) { - var iter = new Generator().each(function(entry, index, data) { - if (index > 0) { - return entry + data[index - 1]; - } else { - return -1; - } - }).iterator(); - - equal(true, Object.isFunc(iter)); - - deepEqual([], [].map(iter)); - deepEqual([-1, 3 + 15, 15 + 8], [3, 15, 8].map(iter)); -}); - -QUnit.test('generators.GeneratorTools.ratio', function(assert) { - // percentage of entry[0] on 1500 written to entry[1] - var iter = GeneratorTools.array.ratio(0, 1500, 100, 1); - deepEqual([[150, 10], [750, 50], [1500, 100]], - [[150], [750], [1500]].map(iter)); - - // percentage of entry[0] on 1500 written to entry[0] - iter = GeneratorTools.array.ratio(0, 1500, 100); - deepEqual([[10], [50], [100]], - [[150], [750], [1500]].map(iter)); -}); - -QUnit.test('generators.GeneratorTools.format', function(assert) { - var iter = GeneratorTools.array.format('Value is %s', 1); - deepEqual([[150, 'Value is 150'], [500, 'Value is 500'], [1000, 'Value is 1000']], - [[150], [500], [1000]].map(iter)); - - iter = GeneratorTools.array.format('Value is %s'); - deepEqual([[150, 'Value is 150'], [500, 'Value is 500'], [1000, 'Value is 1000']], - [[150], [500], [1000]].map(iter)); -}); - -}(jQuery)); diff --git a/creme/settings.py b/creme/settings.py index 5bf7fbf08c..4877479113 100644 --- a/creme/settings.py +++ b/creme/settings.py @@ -867,7 +867,6 @@ 'creme_core/js/lib/fallbacks/event-0.1.js', 'creme_core/js/lib/fallbacks/htmldocument-0.1.js', 'creme_core/js/lib/math.js', - 'creme_core/js/lib/generators-0.1.js', 'creme_core/js/lib/color.js', 'creme_core/js/lib/assert.js', 'creme_core/js/lib/faker.js', @@ -1093,7 +1092,6 @@ 'creme_core/js/tests/dialog/glasspane.js', 'creme_core/js/tests/fallbacks.js', - 'creme_core/js/tests/generators.js', 'creme_core/js/tests/color.js', 'creme_core/js/tests/assert.js', 'creme_core/js/tests/faker.js', From ea7d4e081cb362194d6224de7c1910ebe43df648 Mon Sep 17 00:00:00 2001 From: Fabre Florian Date: Tue, 14 Jan 2025 15:41:01 +0100 Subject: [PATCH 02/29] Deprecate Array.copy(iterable, start, end); Use native code Array.from(iterable).slice(start, end) instead --- CHANGELOG.txt | 2 ++ .../static/creme_core/js/entity_cell.js | 4 +-- .../static/creme_core/js/lib/faker.js | 4 +-- .../creme_core/js/lib/fallbacks/array-0.9.js | 2 ++ .../creme_core/js/lib/fallbacks/console.js | 4 +-- .../creme_core/js/lib/fallbacks/object-0.1.js | 4 +-- .../static/creme_core/js/list_view.core.js | 4 +-- .../js/tests/component/actionregistry.js | 2 +- .../creme_core/js/tests/component/events.js | 6 ++--- .../js/tests/component/qunit-event-mixin.js | 2 +- .../creme_core/js/tests/qunit/qunit-mixin.js | 10 +++---- .../js/tests/qunit/qunit-parametrize.js | 8 +++--- .../static/creme_core/js/tests/views/utils.js | 2 +- .../creme_core/static/creme_core/js/utils.js | 4 +-- .../creme_core/js/widgets/actionlist.js | 4 +-- .../static/creme_core/js/widgets/base.js | 8 +++--- .../js/widgets/component/action-link.js | 10 +++---- .../creme_core/js/widgets/component/action.js | 26 +++++++++---------- .../js/widgets/component/component.js | 4 +-- .../creme_core/js/widgets/component/events.js | 6 ++--- .../creme_core/js/widgets/dialog/dialog.js | 4 +-- .../creme_core/js/widgets/dialog/popover.js | 4 +-- .../creme_core/js/widgets/model/array.js | 6 ++--- .../creme_core/js/widgets/utils/compare.js | 4 +-- .../creme_core/js/widgets/utils/converter.js | 4 +-- .../creme_core/js/widgets/utils/lambda.js | 4 +-- .../creme_core/js/widgets/utils/plugin.js | 4 +-- 27 files changed, 75 insertions(+), 71 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index a26db78aeb..fdd3bc9a72 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -179,6 +179,8 @@ - Add allowEventOverlaps property (boolean or method). - Add allowEventCreate property (boolean). # Javascript: + * Deprecations: + - Array.copy(iterable, start, end) is deprecated; Use native code Array.from(iterable).slice(start, end) instead. * FormDialog: - Form submit error responses with HTML can either replace the default overlay content or the frame content. - New creme.dialog.Frame option 'fillOnError'; if enabled the html error response replaces the content. diff --git a/creme/creme_core/static/creme_core/js/entity_cell.js b/creme/creme_core/static/creme_core/js/entity_cell.js index 0ed61be7c2..b99d282295 100644 --- a/creme/creme_core/static/creme_core/js/entity_cell.js +++ b/creme/creme_core/static/creme_core/js/entity_cell.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2013-2022 Hybird + Copyright (C) 2013-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -30,7 +30,7 @@ creme.entity_cell.EntityCellsWidget = creme.component.Component.sub({ this.column_titles = {}; this.columns = []; this.underlays = {}; - this.samples = Array.copy(options.samples); + this.samples = Array.from(options.samples); }, isBound: function() { diff --git a/creme/creme_core/static/creme_core/js/lib/faker.js b/creme/creme_core/static/creme_core/js/lib/faker.js index 9b5bfd45c2..36f568c044 100644 --- a/creme/creme_core/static/creme_core/js/lib/faker.js +++ b/creme/creme_core/static/creme_core/js/lib/faker.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2020-2022 Hybird + Copyright (C) 2020-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -107,7 +107,7 @@ FunctionFaker.prototype = { var faker = this; return function() { - var args = Array.copy(arguments); + var args = Array.from(arguments); faker._calls.push(args); if (faker._follow) { diff --git a/creme/creme_core/static/creme_core/js/lib/fallbacks/array-0.9.js b/creme/creme_core/static/creme_core/js/lib/fallbacks/array-0.9.js index b685db38c2..49c115191f 100644 --- a/creme/creme_core/static/creme_core/js/lib/fallbacks/array-0.9.js +++ b/creme/creme_core/static/creme_core/js/lib/fallbacks/array-0.9.js @@ -71,6 +71,8 @@ }); appendStatic('copy', function(iterable, start, end) { + console.warn('Deprecated; Use Array.from(iterable).slice(start, end) instead'); + var res = []; start = Math.min(Math.max(0, start || 0), iterable.length); end = end !== undefined ? Math.min(Math.max(0, end), iterable.length) : iterable.length; diff --git a/creme/creme_core/static/creme_core/js/lib/fallbacks/console.js b/creme/creme_core/static/creme_core/js/lib/fallbacks/console.js index cd0df3b858..aeb2ea4ad0 100644 --- a/creme/creme_core/static/creme_core/js/lib/fallbacks/console.js +++ b/creme/creme_core/static/creme_core/js/lib/fallbacks/console.js @@ -1,6 +1,6 @@ /******************************************************************************* * Creme is a free/open-source Customer Relationship Management software - * Copyright (C) 2009-2021 Hybird + * Copyright (C) 2009-2025 Hybird * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License as published by the Free @@ -66,7 +66,7 @@ var _native = Function.prototype.bind .call(console[logger], console); console[logger] = function() { - return _native.apply(console, Array.copy(arguments).map( + return _native.apply(console, Array.from(arguments).map( function(item) { if (typeof item === 'object' && JSON && JSON.stringify) { return JSON.stringify(item, function(key, val) { diff --git a/creme/creme_core/static/creme_core/js/lib/fallbacks/object-0.1.js b/creme/creme_core/static/creme_core/js/lib/fallbacks/object-0.1.js index 39056cb30f..554c382cd3 100644 --- a/creme/creme_core/static/creme_core/js/lib/fallbacks/object-0.1.js +++ b/creme/creme_core/static/creme_core/js/lib/fallbacks/object-0.1.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2009-2022 Hybird + Copyright (C) 2009-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -142,7 +142,7 @@ var proxy = {__context__: context}; var filter = Object.isFunc(options.filter) ? options.filter : function () { return true; }; - var parameters = Object.isFunc(options.arguments) ? function (args) { return options.arguments(Array.copy(args)); } : Array.copy; + var parameters = Object.isFunc(options.arguments) ? function (args) { return options.arguments(Array.from(args)); } : Array.from; for (var key in delegate) { var value = delegate[key]; diff --git a/creme/creme_core/static/creme_core/js/list_view.core.js b/creme/creme_core/static/creme_core/js/list_view.core.js index 49bfbcf37a..bed55734b9 100644 --- a/creme/creme_core/static/creme_core/js/list_view.core.js +++ b/creme/creme_core/static/creme_core/js/list_view.core.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2009-2022 Hybird + Copyright (C) 2009-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free @@ -475,7 +475,7 @@ state: function() { var fields = this._element.find('.lv-state-field:not(.invalid)'); - var names = Array.copy(arguments); + var names = Array.from(arguments); if (names.length > 0) { fields = fields.filter(function() { diff --git a/creme/creme_core/static/creme_core/js/tests/component/actionregistry.js b/creme/creme_core/static/creme_core/js/tests/component/actionregistry.js index 3a6cbeeb2d..9051c34f1d 100644 --- a/creme/creme_core/static/creme_core/js/tests/component/actionregistry.js +++ b/creme/creme_core/static/creme_core/js/tests/component/actionregistry.js @@ -65,7 +65,7 @@ QUnit.module("creme.component.factory.js", new QUnitMixin(QUnitEventMixin, { }, pushActionCall: function() { - this._mockActionCalls.push(Array.copy(arguments)); + this._mockActionCalls.push(Array.from(arguments)); }, mapLinkStartEventType: function(d) { diff --git a/creme/creme_core/static/creme_core/js/tests/component/events.js b/creme/creme_core/static/creme_core/js/tests/component/events.js index b97ac3610c..28af0b3b33 100644 --- a/creme/creme_core/static/creme_core/js/tests/component/events.js +++ b/creme/creme_core/static/creme_core/js/tests/component/events.js @@ -9,7 +9,7 @@ QUnit.module("creme.component.EventHandler.js", new QUnitMixin(QUnitEventMixin, var calls = self._eventListenerCalls; var listenerCalls = calls[name] || []; - listenerCalls.push([this].concat(Array.copy(arguments))); + listenerCalls.push([this].concat(Array.from(arguments))); calls[name] = listenerCalls; }; }(name)); @@ -867,9 +867,9 @@ QUnit.test('creme.component.EventHandler.one (decorator)', function(assert) { }; var decorator = function(key, listener, args) { - handler.trigger(key + '-pre', Array.copy(args, 1)); + handler.trigger(key + '-pre', Array.from(args).slice(1)); listener.apply(this, args); - handler.trigger(key + '-post', Array.copy(args, 1)); + handler.trigger(key + '-post', Array.from(args).slice(1)); }; handler.on(decorator_listeners); diff --git a/creme/creme_core/static/creme_core/js/tests/component/qunit-event-mixin.js b/creme/creme_core/static/creme_core/js/tests/component/qunit-event-mixin.js index f37a737193..48fb8ae1a5 100644 --- a/creme/creme_core/static/creme_core/js/tests/component/qunit-event-mixin.js +++ b/creme/creme_core/static/creme_core/js/tests/component/qunit-event-mixin.js @@ -44,7 +44,7 @@ var self = this; return (function(name) { return function() { - self.mockListenerCalls(name).push(Array.copy(arguments)); + self.mockListenerCalls(name).push(Array.from(arguments)); }; }(name)); }, diff --git a/creme/creme_core/static/creme_core/js/tests/qunit/qunit-mixin.js b/creme/creme_core/static/creme_core/js/tests/qunit/qunit-mixin.js index cd4525f786..53c73edb8d 100644 --- a/creme/creme_core/static/creme_core/js/tests/qunit/qunit-mixin.js +++ b/creme/creme_core/static/creme_core/js/tests/qunit/qunit-mixin.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2017-2024 Hybird + Copyright (C) 2017-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -24,7 +24,7 @@ window.QUnitMixin = function() { var self = this; var reserved = ['setup', 'teardown', 'before', 'after', 'beforeEach', 'afterEach']; - var mixins = this.__mixins = [QUnitBaseMixin].concat(Array.copy(arguments)); + var mixins = this.__mixins = [QUnitBaseMixin].concat(Array.from(arguments)); mixins.forEach(function(mixin) { for (var key in mixin) { @@ -57,7 +57,7 @@ afterEach: function(env) { var self = this; - Array.copy(this.__mixins).reverse().forEach(function(mixin) { + Array.from(this.__mixins).reverse().forEach(function(mixin) { if (Object.isFunc(mixin.afterEach)) { mixin.afterEach.call(self, env); } @@ -241,13 +241,13 @@ var __consoleError = this.__consoleError = console.error; console.warn = function() { - var args = Array.copy(arguments); + var args = Array.from(arguments); self.__consoleWarnCalls.push(args); return __consoleWarn.apply(this, args); }; console.error = function() { - var args = Array.copy(arguments); + var args = Array.from(arguments); self.__consoleErrorCalls.push(args); return __consoleError.apply(this, args); }; diff --git a/creme/creme_core/static/creme_core/js/tests/qunit/qunit-parametrize.js b/creme/creme_core/static/creme_core/js/tests/qunit/qunit-parametrize.js index e5a6221596..169e623ae1 100644 --- a/creme/creme_core/static/creme_core/js/tests/qunit/qunit-parametrize.js +++ b/creme/creme_core/static/creme_core/js/tests/qunit/qunit-parametrize.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2020-2022 Hybird + Copyright (C) 2020-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -48,7 +48,7 @@ window.QUnit.parameterize = function(name, scenarios, callable) { } if (arguments.length > 3) { - var args = Array.copy(arguments); + var args = Array.from(arguments); callable = args[arguments.length - 1]; scenarios = args[1]; var subScenarios = args.slice(2, arguments.length - 1); @@ -60,7 +60,7 @@ window.QUnit.parameterize = function(name, scenarios, callable) { subScenarios ).concat([ function() { - callable.apply(this, (Array.isArray(scenario) ? scenario : [scenario]).concat(Array.copy(arguments))); + callable.apply(this, (Array.isArray(scenario) ? scenario : [scenario]).concat(Array.from(arguments))); } ]) ); @@ -72,7 +72,7 @@ window.QUnit.parameterize = function(name, scenarios, callable) { QUnit.skip('${name}-${label}'.template({name: name, label: label})); } else { QUnit.test('${name}-${label}'.template({name: name, label: label}), function() { - callable.apply(this, (Array.isArray(scenario) ? scenario : [scenario]).concat(Array.copy(arguments))); + callable.apply(this, (Array.isArray(scenario) ? scenario : [scenario]).concat(Array.from(arguments))); }); } }); diff --git a/creme/creme_core/static/creme_core/js/tests/views/utils.js b/creme/creme_core/static/creme_core/js/tests/views/utils.js index 13f695c3b8..a091b35dc4 100644 --- a/creme/creme_core/static/creme_core/js/tests/views/utils.js +++ b/creme/creme_core/static/creme_core/js/tests/views/utils.js @@ -12,7 +12,7 @@ QUnit.module("creme.core.utils.js", new QUnitMixin(QUnitAjaxMixin, var _mockInlineHtmlEventCalls = this._mockInlineHtmlEventCalls = []; QUnit.mockInlineHtmlEvent = function() { - _mockInlineHtmlEventCalls.push(Array.copy(arguments)); + _mockInlineHtmlEventCalls.push(Array.from(arguments)); }; }, afterEach: function() { diff --git a/creme/creme_core/static/creme_core/js/utils.js b/creme/creme_core/static/creme_core/js/utils.js index f19f5ba37c..40f501e222 100644 --- a/creme/creme_core/static/creme_core/js/utils.js +++ b/creme/creme_core/static/creme_core/js/utils.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2009-2024 Hybird + Copyright (C) 2009-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -257,7 +257,7 @@ creme.utils.clickOnce = function(element, func) { if (element.is(':not(.clickonce') && Object.isFunc(func)) { element.addClass('clickonce'); - return func.apply(this, Array.copy(arguments).slice(2)); + return func.apply(this, Array.from(arguments).slice(2)); } else { return false; } diff --git a/creme/creme_core/static/creme_core/js/widgets/actionlist.js b/creme/creme_core/static/creme_core/js/widgets/actionlist.js index 1b5c5bfc33..bd2ec1b0e8 100644 --- a/creme/creme_core/static/creme_core/js/widgets/actionlist.js +++ b/creme/creme_core/static/creme_core/js/widgets/actionlist.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2009-2023 Hybird + Copyright (C) 2009-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -280,7 +280,7 @@ creme.widget.ActionButtonList = creme.widget.declare('ui-creme-actionbuttonlist' element.trigger('actionListSuccess', data); }) .on('cancel fail', function(event) { - element.trigger('actionListCanceled', Array.copy(arguments).slice(1)); + element.trigger('actionListCanceled', Array.from(arguments).slice(1)); }) .one(listeners) .start(); diff --git a/creme/creme_core/static/creme_core/js/widgets/base.js b/creme/creme_core/static/creme_core/js/widgets/base.js index dd8ecf571d..7508375db6 100644 --- a/creme/creme_core/static/creme_core/js/widgets/base.js +++ b/creme/creme_core/static/creme_core/js/widgets/base.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2009-2023 Hybird + Copyright (C) 2009-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -28,7 +28,7 @@ creme.object = { var cb = arguments[0]; if (Object.isFunc(cb)) { - return cb.apply(this, Array.copy(arguments, 1)); + return cb.apply(this, Array.from(arguments).slice(1)); } }, @@ -49,7 +49,7 @@ creme.object = { } if (Object.isFunc(cb)) { - return cb.apply(delegate, Array.copy(arguments, 2)); + return cb.apply(delegate, Array.from(arguments).slice(2)); } }, @@ -196,7 +196,7 @@ $.extend(creme.widget, { } widget[key] = function() { - return value.apply(widget.delegate, [widget.element].concat(Array.copy(arguments))); + return value.apply(widget.delegate, [widget.element].concat(Array.from(arguments))); }; }); })(widget); diff --git a/creme/creme_core/static/creme_core/js/widgets/component/action-link.js b/creme/creme_core/static/creme_core/js/widgets/component/action-link.js index b90e9317c2..d5275ab063 100644 --- a/creme/creme_core/static/creme_core/js/widgets/component/action-link.js +++ b/creme/creme_core/static/creme_core/js/widgets/component/action-link.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2015-2022 Hybird + Copyright (C) 2015-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -70,7 +70,7 @@ creme.action.ActionLink = creme.component.Component.sub({ }, trigger: function(event) { - this._events.trigger(event, Array.copy(arguments).slice(1), this); + this._events.trigger(event, Array.from(arguments).slice(1), this); return this; }, @@ -155,13 +155,13 @@ creme.action.ActionLink = creme.component.Component.sub({ }) .one({ done: function() { - trigger('action-link-done', Array.copy(arguments).slice(1), this); + trigger('action-link-done', Array.from(arguments).slice(1), this); }, cancel: function() { - trigger('action-link-cancel', Array.copy(arguments).slice(1), this); + trigger('action-link-cancel', Array.from(arguments).slice(1), this); }, fail: function() { - trigger('action-link-fail', Array.copy(arguments).slice(1), this); + trigger('action-link-fail', Array.from(arguments).slice(1), this); } }) .start(); diff --git a/creme/creme_core/static/creme_core/js/widgets/component/action.js b/creme/creme_core/static/creme_core/js/widgets/component/action.js index d130187f9e..d9e5200993 100644 --- a/creme/creme_core/static/creme_core/js/widgets/component/action.js +++ b/creme/creme_core/static/creme_core/js/widgets/component/action.js @@ -1,6 +1,6 @@ /******************************************************************************* * Creme is a free/open-source Customer Relationship Management software - * Copyright (C) 2009-2019 Hybird + * Copyright (C) 2009-2025 Hybird * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License as published by the Free @@ -43,9 +43,9 @@ creme.component.Action = creme.component.Component.sub({ } try { - this._events.trigger('start', Array.copy(arguments), this); + this._events.trigger('start', Array.from(arguments), this); this._status = _ActionStatus.RUNNING; - this._action.apply(this, Array.copy(arguments)); + this._action.apply(this, Array.from(arguments)); } catch (e) { console.error(e); this.fail(e); @@ -60,7 +60,7 @@ creme.component.Action = creme.component.Component.sub({ } this._status = _ActionStatus.DONE; - this._events.trigger('done', Array.copy(arguments), this); + this._events.trigger('done', Array.from(arguments), this); return this; }, @@ -70,7 +70,7 @@ creme.component.Action = creme.component.Component.sub({ } this._status = _ActionStatus.FAIL; - this._events.trigger('fail', Array.copy(arguments), this); + this._events.trigger('fail', Array.from(arguments), this); return this; }, @@ -80,12 +80,12 @@ creme.component.Action = creme.component.Component.sub({ } this._status = _ActionStatus.CANCEL; - this._events.trigger('cancel', Array.copy(arguments), this); + this._events.trigger('cancel', Array.from(arguments), this); return this; }, trigger: function(event) { - this._events.trigger(event, Array.copy(arguments).slice(1), this); + this._events.trigger(event, Array.from(arguments).slice(1), this); return this; }, @@ -196,17 +196,17 @@ creme.component.Action = creme.component.Component.sub({ var self = this; source.onDone(function() { - self.start.apply(self, options.passArgs ? Array.copy(arguments).slice(1) : []); + self.start.apply(self, options.passArgs ? Array.from(arguments).slice(1) : []); }); source.onFail(function(event) { self._status = _ActionStatus.FAIL; - self._events.trigger('fail', Array.copy(arguments).slice(1), self); + self._events.trigger('fail', Array.from(arguments).slice(1), self); }); source.onCancel(function(event) { self._status = _ActionStatus.CANCEL; - self._events.trigger('cancel', Array.copy(arguments).slice(1), self); + self._events.trigger('cancel', Array.from(arguments).slice(1), self); }); return this; @@ -222,17 +222,17 @@ creme.component.Action = creme.component.Component.sub({ delegate.onDone(function() { self._status = _ActionStatus.DONE; - self._events.trigger('done', Array.copy(arguments).slice(1), self); + self._events.trigger('done', Array.from(arguments).slice(1), self); }); delegate.onFail(function() { self._status = _ActionStatus.FAIL; - self._events.trigger('fail', Array.copy(arguments).slice(1), self); + self._events.trigger('fail', Array.from(arguments).slice(1), self); }); delegate.onCancel(function() { self._status = _ActionStatus.CANCEL; - self._events.trigger('cancel', Array.copy(arguments).slice(1), self); + self._events.trigger('cancel', Array.from(arguments).slice(1), self); }); return this; diff --git a/creme/creme_core/static/creme_core/js/widgets/component/component.js b/creme/creme_core/static/creme_core/js/widgets/component/component.js index 3861aece73..207d4ebbf8 100644 --- a/creme/creme_core/static/creme_core/js/widgets/component/component.js +++ b/creme/creme_core/static/creme_core/js/widgets/component/component.js @@ -1,6 +1,6 @@ /******************************************************************************* * Creme is a free/open-source Customer Relationship Management software - * Copyright (C) 2009-2021 Hybird + * Copyright (C) 2009-2025 Hybird * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License as published by the Free @@ -67,7 +67,7 @@ creme.component.Component = creme.component.extend(Object, { _super_: function(constructor, method) { if (method !== undefined) { - return constructor.prototype[method].apply(this, Array.copy(arguments).slice(2)); + return constructor.prototype[method].apply(this, Array.from(arguments).slice(2)); } return Object.proxy(constructor.prototype, this); diff --git a/creme/creme_core/static/creme_core/js/widgets/component/events.js b/creme/creme_core/static/creme_core/js/widgets/component/events.js index cd49fff0f5..283bae9d96 100644 --- a/creme/creme_core/static/creme_core/js/widgets/component/events.js +++ b/creme/creme_core/static/creme_core/js/widgets/component/events.js @@ -1,6 +1,6 @@ /******************************************************************************* * Creme is a free/open-source Customer Relationship Management software - * Copyright (C) 2009-2022 Hybird + * Copyright (C) 2009-2025 Hybird * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License as published by the Free @@ -90,7 +90,7 @@ creme.component.EventHandler = creme.component.Component.sub({ if (Object.isFunc(decorator)) { var proxy = (function(key, listener, decorator) { return function() { - return decorator.apply(this, [key, listener, Array.copy(arguments)]); + return decorator.apply(this, [key, listener, Array.from(arguments)]); }; })(key, listener, decorator); @@ -185,7 +185,7 @@ creme.component.EventHandler = creme.component.Component.sub({ var args = [key].concat(data); var error = this._error.bind(source); - Array.copy(this.listeners(key)).forEach(function(listener) { + Array.from(this.listeners(key)).forEach(function(listener) { try { listener.apply(source, args); } catch (e) { diff --git a/creme/creme_core/static/creme_core/js/widgets/dialog/dialog.js b/creme/creme_core/static/creme_core/js/widgets/dialog/dialog.js index d1408ef97d..d2e8dfcd60 100644 --- a/creme/creme_core/static/creme_core/js/widgets/dialog/dialog.js +++ b/creme/creme_core/static/creme_core/js/widgets/dialog/dialog.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2009-2024 Hybird + Copyright (C) 2009-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -531,7 +531,7 @@ creme.dialog.Dialog = creme.component.Component.sub({ }, trigger: function(event) { - var data = Array.copy(arguments).slice(1); + var data = Array.from(arguments).slice(1); if (this.options.propagateEvent) { /* diff --git a/creme/creme_core/static/creme_core/js/widgets/dialog/popover.js b/creme/creme_core/static/creme_core/js/widgets/dialog/popover.js index ac2fd3b07d..ff978b771a 100644 --- a/creme/creme_core/static/creme_core/js/widgets/dialog/popover.js +++ b/creme/creme_core/static/creme_core/js/widgets/dialog/popover.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2017-2022 Hybird + Copyright (C) 2017-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -179,7 +179,7 @@ creme.dialog.Popover = creme.component.Component.sub({ creme.utils.scrollBack(this._scrollbackPosition, 'slow'); this._scrollbackPosition = null; - this._events.trigger('closed', Array.copy(arguments), this); + this._events.trigger('closed', Array.from(arguments), this); return this; }, diff --git a/creme/creme_core/static/creme_core/js/widgets/model/array.js b/creme/creme_core/static/creme_core/js/widgets/model/array.js index 009984226f..bb61b3a82f 100644 --- a/creme/creme_core/static/creme_core/js/widgets/model/array.js +++ b/creme/creme_core/static/creme_core/js/widgets/model/array.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2009-2023 Hybird + Copyright (C) 2009-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -252,7 +252,7 @@ creme.model.Array = creme.model.Collection.sub({ sort: function(comparator) { var data = this._data; - var previous = Array.copy(data); + var previous = Array.from(data); comparator = comparator || this._comparator; data.sort(comparator); @@ -265,7 +265,7 @@ creme.model.Array = creme.model.Collection.sub({ reverse: function() { var data = this._data; - var previous = Array.copy(data); + var previous = Array.from(data); data.reverse(); diff --git a/creme/creme_core/static/creme_core/js/widgets/utils/compare.js b/creme/creme_core/static/creme_core/js/widgets/utils/compare.js index f1ea0e9bab..993a2b7588 100644 --- a/creme/creme_core/static/creme_core/js/widgets/utils/compare.js +++ b/creme/creme_core/static/creme_core/js/widgets/utils/compare.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2009-2011 Hybird + Copyright (C) 2009-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -26,7 +26,7 @@ creme.utils.comparator = function() { return creme.utils.compareTo; } - var attributes = Array.copy(arguments); + var attributes = Array.from(arguments); return function(a, b) { for (var i = 0; i < attributes.length; ++i) { diff --git a/creme/creme_core/static/creme_core/js/widgets/utils/converter.js b/creme/creme_core/static/creme_core/js/widgets/utils/converter.js index f4f3b11e06..bd4f8d1773 100644 --- a/creme/creme_core/static/creme_core/js/widgets/utils/converter.js +++ b/creme/creme_core/static/creme_core/js/widgets/utils/converter.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2009-2021 Hybird + Copyright (C) 2009-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -76,7 +76,7 @@ creme.utils.ConverterRegistry = creme.component.Component.sub({ } if (Array.isArray(from)) { - var args = Array.copy(arguments).slice(1); + var args = Array.from(arguments).slice(1); from.forEach(function(f) { this.register.apply(this, [f].concat(args)); diff --git a/creme/creme_core/static/creme_core/js/widgets/utils/lambda.js b/creme/creme_core/static/creme_core/js/widgets/utils/lambda.js index d7c44093ed..1e30509afb 100644 --- a/creme/creme_core/static/creme_core/js/widgets/utils/lambda.js +++ b/creme/creme_core/static/creme_core/js/widgets/utils/lambda.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2009-2022 Hybird + Copyright (C) 2009-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -38,7 +38,7 @@ creme.utils.Lambda = creme.component.Component.sub({ call: function() { if (this._lambda) { - var args = Array.copy(arguments); + var args = Array.from(arguments); return this._lambda.apply(args[0], args.slice(1)); } }, diff --git a/creme/creme_core/static/creme_core/js/widgets/utils/plugin.js b/creme/creme_core/static/creme_core/js/widgets/utils/plugin.js index 47c2455527..1978a6ee86 100644 --- a/creme/creme_core/static/creme_core/js/widgets/utils/plugin.js +++ b/creme/creme_core/static/creme_core/js/widgets/utils/plugin.js @@ -1,6 +1,6 @@ /******************************************************************************* * Creme is a free/open-source Customer Relationship Management software - * Copyright (C) 2020-2022 Hybird + * Copyright (C) 2020-2025 Hybird * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License as published by the Free @@ -72,7 +72,7 @@ creme.utils.newJQueryPlugin = function(options) { }); $.fn[name] = function(methodname, key, value) { - var args = Array.copy(arguments); + var args = Array.from(arguments); var resultList = this.get().map(function(element) { var instanceKey = '-' + name; From 549e5c26e6d9f072a8f8c2b237da358de98e8f65 Mon Sep 17 00:00:00 2001 From: Fabre Florian Date: Tue, 14 Jan 2025 15:54:13 +0100 Subject: [PATCH 03/29] Drop fallbacks for widely implemented Array methods --- CHANGELOG.txt | 9 + .../creme_core/js/lib/fallbacks/array-0.9.js | 193 ++---------------- .../static/creme_core/js/tests/fallbacks.js | 23 +-- .../creme_core/js/widgets/chainedselect.js | 4 +- 4 files changed, 31 insertions(+), 198 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index fdd3bc9a72..d482e75601 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -217,6 +217,15 @@ - "creme_core-hatmenubar-view" - Javascript : * ArrayTools & GeneratorTools are no longer used (only by jqplot). So drop them and use underscore or d3 instead. + * Drop fallbacks or helpers for widely implemented Array methods : + - Use Array.prototype.some() native implementation. + - Array.prototype.unique() is removed; Use _.uniq(iterable) instead. + - Array.prototype.removeAt() is removed; Use Array.prototype.splice(index, 1) instead. + - Array.prototype.insertAt() is removed; Use Array.prototype.splice(index, 0, val1, val2, ...) instead. + - Array.prototype.inArray() is removed; Use Array.prototype.indexOf(value) instead. + - Array.prototype.getRange() is removed; Use Array.prototype.slice() instead. + - Array.prototype.exfiltrate() is removed; Use _.without(iterable, val1, val2, ...) instead. + - Array.prototype.contains() is removed; Use Array.prototype.includes(value) instead. - Apps : * Activities: - Refactor creme.ActivityCalendar : can be now used as independent component. diff --git a/creme/creme_core/static/creme_core/js/lib/fallbacks/array-0.9.js b/creme/creme_core/static/creme_core/js/lib/fallbacks/array-0.9.js index 49c115191f..7ad2d56e67 100644 --- a/creme/creme_core/static/creme_core/js/lib/fallbacks/array-0.9.js +++ b/creme/creme_core/static/creme_core/js/lib/fallbacks/array-0.9.js @@ -25,51 +25,19 @@ * * @private */ + /* function append(name, method) { if (!Array.prototype[name]) { Array.prototype[name] = method; } }; - +*/ function appendStatic(name, method) { if (!Array[name]) { Array[name] = method; } }; - /** - * Returns the first index at which a given element can be found in the array, or -1 if it is not present. - * - * @example [12, 5, 8, 5, 44].indexOf(5); - * @result 1; - * - * @example [12, 5, 8, 5, 44].indexOf(5, 2); - * @result 3; - * - * @name indexOf - * @param Object subject Object to search for - * @param Number offset (optional) Index at which to start searching - * @return Int - */ - /* istanbul ignore next : already supported by any recent browsers */ - append("indexOf", function(subject, offset) { - for (var i = offset || 0; i < this.length; i++) { - if (this[i] === subject) { - return i; - } - } - - return -1; - }); - - // ES5 15.4.3.2 - // http://es5.github.com/#x15.4.3.2 - // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/isArray - /* istanbul ignore next */ - appendStatic('isArray', function(obj) { - return (obj !== undefined && obj !== null && obj.toString() === "[object Array]") || obj instanceof Array; - }); - appendStatic('copy', function(iterable, start, end) { console.warn('Deprecated; Use Array.from(iterable).slice(start, end) instead'); @@ -83,152 +51,11 @@ return res; }); - - /** - * Creates a new array with the results of calling a provided function on every element in this array. - * - * Natively supported in Gecko since version 1.8. - * http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Objects:Array:map - * - * @example ["my", "Name", "is", "HARRY"].map(function(element, index, array) { - * return element.toUpperCase(); - * }); - * @result ["MY", "NAME", "IS", "HARRY"]; - * - * @example [1, 4, 9].map(Math.sqrt); - * @result [1, 2, 3]; - * - * @name map - * @param Function fn The function to be called for each element. - * @param Object scope (optional) The scope of the function (defaults to this). - * @return Array - */ - /* istanbul ignore next : already supported by any recent browsers */ - append("map", function(fn, scope) { - scope = scope || window; - var r = []; - for (var i = 0; i < this.length; i++) { - r[r.length] = fn.call(scope, this[i], i, this); - } - - return r; - }); - - /** - * Executes a provided function once per array element. - * - * Natively supported in Gecko since version 1.8. - * http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Objects:Array:forEach - * - * @example var stuff = ""; - * ["Java", "Script"].forEach(function(element, index, array) { - * stuff += element; - * }); - * @result "JavaScript"; - * - * @name forEach - * @param Function fn The function to be called for each element. - * @param Object scope (optional) The scope of the function (defaults to this). - * @return void - */ - /* istanbul ignore next : already supported by any recent browsers */ - append("forEach", function(fn, scope) { - for (var i = 0; i < this.length; i++) { - fn.call(scope || window, this[i], i, this); - } - }); - +/* if ($ !== undefined) { return; } - - /** - * Returns the array without the elements in 'elements'. - * - * @example [1, 2, 1, 4, 5, 4].contains([1, 2, 4]); - * @result true - * - * @name exfiltrate - * @param Array elements - * @return Boolean - */ - /* istanbul ignore next */ - append("exfiltrate", function(elements) { - return this.filter(function(element) { - return this.indexOf(element) < 0; - }, elements); - }); - - /** - * Returns true if every element in 'elements' is in the array. - * - * @example [1, 2, 1, 4, 5, 4].contains(1); - * @result true - * - * @name contains - * @param Array elements - * @return Boolean - */ - /* istanbul ignore next : already supported by any recent browsers */ - append("contains", function(element) { - return this.indexOf(element) >= 0; - }); - - /** - * Tests whether all elements in the array pass the test implemented by the provided function. - * - * @example [22, 72, 16, 99, 254].every(function(element, index, array) { - * return element >= 15; - * }); - * @result true; - * - * @example [12, 72, 16, 99, 254].every(function(element, index, array) { - * return element >= 15; - * }); - * @result false; - * - * @name every - * @param Function fn The function to be called for each element. - * @param Object scope (optional) The scope of the function (defaults to this). - * @return Boolean - */ - /* istanbul ignore next */ - append("every", function(fn, scope) { - for (var i = 0; i < this.length; i++) { - if (!fn.call(scope || window, this[i], i, this)) { - return false; - } - } - return true; - }); - - /** - * Creates a new array with all elements that pass the test implemented by the provided function. - * - * Natively supported in Gecko since version 1.8. - * http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Objects:Array:filter - * - * @example [12, 5, 8, 1, 44].filter(function(element, index, array) { - * return element >= 10; - * }); - * @result [12, 44]; - * - * @name filter - * @param Function fn The function to be called for each element. - * @param Object scope (optional) The scope of the function (defaults to this). - * @return Array - */ - /* istanbul ignore next */ - append("filter", function(fn, scope) { - var r = []; - for (var i = 0; i < this.length; i++) { - if (fn.call(scope || window, this[i], i, this)) { - r.push(this[i]); - } - } - return r; - }); - +*/ /** * Returns a range of items in this collection * @@ -241,6 +68,7 @@ * @return Array */ /* istanbul ignore next */ + /* append("getRange", function(start, end) { var items = this; if (items.length < 1) { @@ -262,6 +90,7 @@ return r; }); + */ /** * Checks if a given subject can be found in the array. @@ -277,6 +106,7 @@ * @return Boolean */ /* istanbul ignore next */ + /* append("inArray", function(subject) { for (var i = 0; i < this.length; i++) { if (subject === this[i]) { @@ -285,6 +115,7 @@ } return false; }); + */ /** * Inserts an item at the specified index in the array. @@ -298,6 +129,7 @@ * @return Array */ /* istanbul ignore next */ + /* append("insertAt", function(index, element) { for (var k = this.length; k > index; k--) { this[k] = this[k - 1]; @@ -306,6 +138,7 @@ this[index] = element; return this; }); + */ /** * Remove an item from a specified index in the array. @@ -318,6 +151,7 @@ * @return Array */ /* istanbul ignore next */ + /* append("removeAt", function(index) { for (var k = index; k < this.length - 1; k++) { this[k] = this[k + 1]; @@ -326,6 +160,7 @@ this.length--; return this; }); + */ /** * Tests whether some element in the array passes the test implemented by the provided function. @@ -349,6 +184,7 @@ * @return Boolean */ /* istanbul ignore next */ + /* append("some", function(fn, scope) { for (var i = 0; i < this.length; i++) { if (fn.call(scope || window, this[i], i, this)) { @@ -358,6 +194,7 @@ return false; }); + */ /** * Returns a new array that contains all unique elements of this array. @@ -369,9 +206,11 @@ * @return Array */ /* istanbul ignore next */ + /* append("unique", function() { return this.filter(function(element, index, array) { return array.indexOf(element) >= index; }); }); + */ }(jQuery)); diff --git a/creme/creme_core/static/creme_core/js/tests/fallbacks.js b/creme/creme_core/static/creme_core/js/tests/fallbacks.js index 2f780f92a9..b20ec7259f 100644 --- a/creme/creme_core/static/creme_core/js/tests/fallbacks.js +++ b/creme/creme_core/static/creme_core/js/tests/fallbacks.js @@ -500,22 +500,6 @@ QUnit.test('fallbacks.Array.copy (arguments)', function() { }); if (jQuery === undefined) { - QUnit.test('fallbacks.Array.contains', function() { - equal(typeof Array.prototype.contains, 'function'); - equal(typeof [].contains, 'function'); - - equal([1, 2, 1, 4, 5, 4].contains(1), true); - equal([1, 2, 1, 4, 5, 4].contains(2), true); - equal([1, 2, 1, 4, 5, 4].contains(12), false); - }); - - QUnit.test('fallbacks.Array.exfiltrate', function() { - equal(typeof Array.prototype.exfiltrate, 'function'); - equal(typeof [].exfiltrate, 'function'); - - deepEqual([1, 2, 1, 4, 5, 4].exfiltrate([1, 2]), [4, 5, 4]); - }); - QUnit.test('fallbacks.Array.every', function() { equal(typeof Array.prototype.every, 'function'); equal(typeof [].every, 'function'); @@ -539,7 +523,7 @@ if (jQuery === undefined) { return element >= 10; }), [12, 44]); }); - +/* QUnit.test('fallbacks.Array.getRange', function() { equal(typeof Array.prototype.getRange, 'function'); equal(typeof [].getRange, 'function'); @@ -569,7 +553,7 @@ if (jQuery === undefined) { deepEqual(['dog', 'cat', 'mouse', 'horse'].removeAt(2), ['dog', 'cat', 'horse']); }); - +*/ QUnit.test('fallbacks.Array.some', function() { equal(typeof Array.prototype.some, 'function'); equal(typeof [].some, 'function'); @@ -582,13 +566,14 @@ if (jQuery === undefined) { return element >= 100; }), false); }); - +/* QUnit.test('fallbacks.Array.unique', function() { equal(typeof Array.prototype.unique, 'function'); equal(typeof [].unique, 'function'); deepEqual([1, 2, 1, 4, 5, 4].unique(), [1, 2, 4, 5]); }); +*/ } QUnit.test('fallbacks.HTMLDocument', function() { diff --git a/creme/creme_core/static/creme_core/js/widgets/chainedselect.js b/creme/creme_core/static/creme_core/js/widgets/chainedselect.js index d711728bba..ac22c19d85 100644 --- a/creme/creme_core/static/creme_core/js/widgets/chainedselect.js +++ b/creme/creme_core/static/creme_core/js/widgets/chainedselect.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2009-2022 Hybird + Copyright (C) 2009-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -139,7 +139,7 @@ creme.widget.ChainedSelect = creme.widget.declare('ui-creme-chainedselect', { var ready = target.is('.widget-ready'); var widget = target.creme().widget(); - if (ready && $.inArray(name, widget.dependencies()) !== -1) { + if (ready && (widget.dependencies() || []).indexOf(name) !== -1) { widget.reload(data, undefined, undefined, true); } }, From f6b5c5fa3b295a7d70829c680ede9eedda2b278f Mon Sep 17 00:00:00 2001 From: joehybird Date: Tue, 14 Jan 2025 17:50:10 +0100 Subject: [PATCH 04/29] Remove fallbacks for HTMLDocument, CSSStyleDeclaration & Event classes --- CHANGELOG.txt | 3 ++ .../creme_core/js/lib/fallbacks/event-0.1.js | 48 ------------------- .../js/lib/fallbacks/htmldocument-0.1.js | 43 ----------------- creme/settings.py | 2 - 4 files changed, 3 insertions(+), 93 deletions(-) delete mode 100644 creme/creme_core/static/creme_core/js/lib/fallbacks/event-0.1.js delete mode 100644 creme/creme_core/static/creme_core/js/lib/fallbacks/htmldocument-0.1.js diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d482e75601..9299723ce4 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -3,6 +3,8 @@ UPGRADE NOTES : - If you upgrade your MariaDB server to 10.7+, you have to convert all the UUID fields (which were just CharFields before MariaDB 10.7) to real UUID fields. + - Many fallbacks and helpers for older browsers (e.g IE 9 & 10) are removed. Read carefully the changelog, it + contains tips to upgrade your code. Users side : ------------ @@ -226,6 +228,7 @@ - Array.prototype.getRange() is removed; Use Array.prototype.slice() instead. - Array.prototype.exfiltrate() is removed; Use _.without(iterable, val1, val2, ...) instead. - Array.prototype.contains() is removed; Use Array.prototype.includes(value) instead. + * Remove fallbacks for HTMLDocument, CSSStyleDeclaration & Event classes. - Apps : * Activities: - Refactor creme.ActivityCalendar : can be now used as independent component. diff --git a/creme/creme_core/static/creme_core/js/lib/fallbacks/event-0.1.js b/creme/creme_core/static/creme_core/js/lib/fallbacks/event-0.1.js deleted file mode 100644 index 5c3abcd5c6..0000000000 --- a/creme/creme_core/static/creme_core/js/lib/fallbacks/event-0.1.js +++ /dev/null @@ -1,48 +0,0 @@ -/******************************************************************************* - Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2009-2022 Hybird - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - *******************************************************************************/ - -/* istanbul ignore next : compatibility with IE < 8.0 */ -(function() { - "use strict"; - - if (!window['Event']) { - window['Event'] = function() {}; - } - - function append(klass, name, method) { - if (!klass.prototype[name]) { - klass.prototype[name] = method; - } - }; - - append(Event, 'stopPropagation', function() { - this.cancelBubble = true; - }); - - append(Event, 'preventDefault', function() { - this.returnValue = false; - }); - - if (!window['ResizeObserver']) { - window['ResizeObserver'] = function() {}; - } - - append(ResizeObserver, 'observe', function() {}); - append(ResizeObserver, 'unobserve', function() {}); - append(ResizeObserver, 'disconnect', function() {}); -}()); diff --git a/creme/creme_core/static/creme_core/js/lib/fallbacks/htmldocument-0.1.js b/creme/creme_core/static/creme_core/js/lib/fallbacks/htmldocument-0.1.js deleted file mode 100644 index bc7c5e0100..0000000000 --- a/creme/creme_core/static/creme_core/js/lib/fallbacks/htmldocument-0.1.js +++ /dev/null @@ -1,43 +0,0 @@ -/******************************************************************************* - Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2009-2017 Hybird - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - *******************************************************************************/ - -// HTMLDocument = $.assertIEVersions(7, 8, 9) ? function() {} : HTMLDocument; -// CSSStyleDeclaration = $.assertIEVersions(7) ? function() {} : CSSStyleDeclaration; - -/* istanbul ignore next : compatibility with IE < 8.0 */ -(function() { - "use strict"; - - if (!window['HTMLDocument']) { - window['HTMLDocument'] = function () {}; - } - - if (!window['CSSStyleDeclaration']) { - window['CSSStyleDeclaration'] = function () {}; - } - - function append(name, method) { - if (!HTMLDocument.prototype[name]) { - HTMLDocument.prototype[name] = method; - } - }; - - append("createElementNS", function(namespace, name) { - return this.createElement(name); - }); -}()); diff --git a/creme/settings.py b/creme/settings.py index 4877479113..ac8fab66c5 100644 --- a/creme/settings.py +++ b/creme/settings.py @@ -864,8 +864,6 @@ 'creme_core/js/lib/fallbacks/array-0.9.js', 'creme_core/js/lib/fallbacks/string-0.1.js', 'creme_core/js/lib/fallbacks/console.js', - 'creme_core/js/lib/fallbacks/event-0.1.js', - 'creme_core/js/lib/fallbacks/htmldocument-0.1.js', 'creme_core/js/lib/math.js', 'creme_core/js/lib/color.js', 'creme_core/js/lib/assert.js', From 99675efa3833a1adcea3db1cbda2052dcee78649 Mon Sep 17 00:00:00 2001 From: joehybird Date: Wed, 15 Jan 2025 07:24:02 +0100 Subject: [PATCH 05/29] Drop fallbacks for widely implemented String methods : trim, ltrim, rtrim, startsWith, endsWith --- CHANGELOG.txt | 6 ++++++ .../creme_core/js/lib/fallbacks/string-0.1.js | 21 +++---------------- .../static/creme_core/js/tests/fallbacks.js | 4 ++-- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 9299723ce4..f564126c53 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -229,6 +229,12 @@ - Array.prototype.exfiltrate() is removed; Use _.without(iterable, val1, val2, ...) instead. - Array.prototype.contains() is removed; Use Array.prototype.includes(value) instead. * Remove fallbacks for HTMLDocument, CSSStyleDeclaration & Event classes. + * Drop fallbacks or helpers for widely implemented String methods : + - Use Array.prototype.startsWith() native implementation. + - Use Array.prototype.endsWith() native implementation. + - Use Array.prototype.trim() native implementation. + - Array.prototype.ltrim() is removed; Use Array.prototype.trimStart() instead. + - Array.prototype.rtrim() is removed; Use Array.prototype.trimEnd() instead. - Apps : * Activities: - Refactor creme.ActivityCalendar : can be now used as independent component. diff --git a/creme/creme_core/static/creme_core/js/lib/fallbacks/string-0.1.js b/creme/creme_core/static/creme_core/js/lib/fallbacks/string-0.1.js index 28f2e91be3..3c0bbd12f2 100644 --- a/creme/creme_core/static/creme_core/js/lib/fallbacks/string-0.1.js +++ b/creme/creme_core/static/creme_core/js/lib/fallbacks/string-0.1.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2009-2020 Hybird + Copyright (C) 2009-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -24,22 +24,7 @@ String.prototype[name] = method; } }; - - /* istanbul ignore next */ - append('startsWith', function (str) { - return this.slice(0, str.length) === str; - }); - - /* istanbul ignore next */ - append('endsWith', function (str) { - return this.slice(-str.length) === str; - }); - - /* istanbul ignore next */ - append('trim', function() { - return this.replace(/^\s+|\s+$/g, ''); - }); - +/* append('ltrim', function() { return this.replace(/^\s+/, ''); }); @@ -47,7 +32,7 @@ append('rtrim', function() { return this.replace(/\s+$/, ''); }); - +*/ append('capitalize', function() { return this.length > 0 ? this.slice(0, 1).toUpperCase().concat(this.slice(1, this.length)) : this; }); diff --git a/creme/creme_core/static/creme_core/js/tests/fallbacks.js b/creme/creme_core/static/creme_core/js/tests/fallbacks.js index b20ec7259f..7526983d38 100644 --- a/creme/creme_core/static/creme_core/js/tests/fallbacks.js +++ b/creme/creme_core/static/creme_core/js/tests/fallbacks.js @@ -595,7 +595,7 @@ QUnit.test('fallbacks.Event', function() { QUnit.test('fallbacks.Event.preventDefault', function() { equal(typeof Event.prototype.preventDefault, 'function'); }); - +/* QUnit.test('fallbacks.String.trim', function() { equal('', ''.trim()); equal('', ' '.trim()); @@ -660,7 +660,7 @@ QUnit.test('fallbacks.String.endsWith', function() { equal('dcba'.endsWith('d'), false); equal('dcba'.startsWith('abcd'), false); }); - +*/ QUnit.test('fallbacks.String.format (skip format)', function() { equal('%d', '%%d'.format(12)); equal('%12', '%%%d'.format(12)); From 1137fd97c9bf9571a9a9ac2feaa438ecabdb952b Mon Sep 17 00:00:00 2001 From: Fabre Florian Date: Wed, 15 Jan 2025 11:49:41 +0100 Subject: [PATCH 06/29] Drop fallbacks for console methods (IE 9 & 10 compatibility) --- CHANGELOG.txt | 1 + .../creme_core/js/lib/fallbacks/console.js | 88 ------------------- creme/settings.py | 1 - 3 files changed, 1 insertion(+), 89 deletions(-) delete mode 100644 creme/creme_core/static/creme_core/js/lib/fallbacks/console.js diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f564126c53..465e09c7ab 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -235,6 +235,7 @@ - Use Array.prototype.trim() native implementation. - Array.prototype.ltrim() is removed; Use Array.prototype.trimStart() instead. - Array.prototype.rtrim() is removed; Use Array.prototype.trimEnd() instead. + * Drop fallbacks for console methods: log(), warn() & error() - Apps : * Activities: - Refactor creme.ActivityCalendar : can be now used as independent component. diff --git a/creme/creme_core/static/creme_core/js/lib/fallbacks/console.js b/creme/creme_core/static/creme_core/js/lib/fallbacks/console.js deleted file mode 100644 index aeb2ea4ad0..0000000000 --- a/creme/creme_core/static/creme_core/js/lib/fallbacks/console.js +++ /dev/null @@ -1,88 +0,0 @@ -/******************************************************************************* - * Creme is a free/open-source Customer Relationship Management software - * Copyright (C) 2009-2025 Hybird - * - * This program is free software: you can redistribute it and/or modify it under - * the terms of the GNU Affero General Public License as published by the Free - * Software Foundation, either version 3 of the License, or (at your option) any - * later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more - * details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - ******************************************************************************/ - -(function() { - "use strict"; - - window.console = window.console || {}; - - function appendStatic(name, method) { - /* istanbul ignore next */ - if (!window.console[name]) { - window.console[name] = method; - } - } - - /* istanbul ignore next */ - function isIE() { - var matches = navigator.appVersion.match(/MSIE ([\d.]+)/); - - if (matches === null) { - return false; - } - - if (arguments.length === 0) { - return true; - } - - for (var i = 0; i < arguments.length; ++i) { - if (matches[1].indexOf('' + arguments[i]) !== -1) { - return true; - } - } - - return false; - } - - /* istanbul ignore next */ - appendStatic('log', function() { - if (window.opera && window.opera.postError) { - return window.opera.postError.apply(window.opera, arguments); - } - }); - - appendStatic('warn', window.console.log); - appendStatic('error', window.console.log); - - /* istanbul ignore next */ - if (isIE(9, 10)) { - var methods = [ 'log', 'warn', 'error' ]; - methods.forEach(function(logger) { - var _native = Function.prototype.bind - .call(console[logger], console); - console[logger] = function() { - return _native.apply(console, Array.from(arguments).map( - function(item) { - if (typeof item === 'object' && JSON && JSON.stringify) { - return JSON.stringify(item, function(key, val) { - if (typeof val === 'function') { - val = val.toString(); - return val.slice(0, - val.indexOf(')') + 1); - } else { - return val; - } - }) + ' '; - } - - return item + ' '; - })); - }; - }); - } -}()); diff --git a/creme/settings.py b/creme/settings.py index ac8fab66c5..8efb10ccf6 100644 --- a/creme/settings.py +++ b/creme/settings.py @@ -863,7 +863,6 @@ 'creme_core/js/lib/fallbacks/object-0.1.js', 'creme_core/js/lib/fallbacks/array-0.9.js', 'creme_core/js/lib/fallbacks/string-0.1.js', - 'creme_core/js/lib/fallbacks/console.js', 'creme_core/js/lib/math.js', 'creme_core/js/lib/color.js', 'creme_core/js/lib/assert.js', From 9940c341f79d661efac472291dc3d6933bf76ab0 Mon Sep 17 00:00:00 2001 From: Fabre Florian Date: Wed, 15 Jan 2025 11:51:35 +0100 Subject: [PATCH 07/29] Use native Object.assign instead of $.extend for libs. --- CHANGELOG.txt | 1 + .../static/creme_core/js/lib/assert.js | 16 ++++++++-------- .../static/creme_core/js/lib/browser.js | 6 +++--- .../creme_core/static/creme_core/js/lib/color.js | 8 ++++---- .../creme_core/static/creme_core/js/lib/faker.js | 4 ++-- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 465e09c7ab..c3abe3fdde 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -183,6 +183,7 @@ # Javascript: * Deprecations: - Array.copy(iterable, start, end) is deprecated; Use native code Array.from(iterable).slice(start, end) instead. + * Use Object.assign instead of $.extend to remove the dependency to jQuery for : RGBColor, DateFaker, BrowserVersion & Assert. * FormDialog: - Form submit error responses with HTML can either replace the default overlay content or the frame content. - New creme.dialog.Frame option 'fillOnError'; if enabled the html error response replaces the content. diff --git a/creme/creme_core/static/creme_core/js/lib/assert.js b/creme/creme_core/static/creme_core/js/lib/assert.js index f06e503fff..b8c64e85c0 100644 --- a/creme/creme_core/static/creme_core/js/lib/assert.js +++ b/creme/creme_core/static/creme_core/js/lib/assert.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2020 Hybird + Copyright (C) 2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -16,7 +16,7 @@ along with this program. If not, see . *******************************************************************************/ -(function($) { +(function() { "use strict"; var __eval = function(value) { @@ -66,14 +66,14 @@ window.Assert = { return test(); } catch (e) { throw new Error((message || '${error}').template( - $.extend({error: e.message}, context || {}) + Object.assign({error: e.message}, context || {}) )); } }, in: function(value, data, message, context) { message = message || "${value} is not in the collection"; - context = $.extend({}, context || {}, {value: value}); + context = Object.assign({}, context || {}, {value: value}); if (Array.isArray(data) || Object.isString(data)) { Assert.that(data.indexOf(value) !== -1, message, context); @@ -86,7 +86,7 @@ window.Assert = { notIn: function(value, data, message, context) { message = message || "${value} should not be in the collection"; - context = $.extend({}, context || {}, {value: value}); + context = Object.assign({}, context || {}, {value: value}); if (Array.isArray(data) || Object.isString(data)) { Assert.that(data.indexOf(value) === -1, message, context); @@ -99,7 +99,7 @@ window.Assert = { is: function(value, expected, message, context) { message = message || '${value} is not a ${expected}'; - context = $.extend({}, context || {}, { + context = Object.assign({}, context || {}, { value: Object.isString(value) ? '"' + value + '"' : value, expected: __fmtType(expected) }); @@ -110,7 +110,7 @@ window.Assert = { isAnyOf: function(value, expected, message, context) { message = message || '${value} is none of [${expected}]'; - context = $.extend({}, context || {}, { + context = Object.assign({}, context || {}, { value: Object.isString(value) ? '"' + value + '"' : value, expected: expected.map(__fmtType).join(', ') }); @@ -126,4 +126,4 @@ window.Assert = { } }; -}(jQuery)); +}()); diff --git a/creme/creme_core/static/creme_core/js/lib/browser.js b/creme/creme_core/static/creme_core/js/lib/browser.js index 8aced5b89c..c4185f8e06 100644 --- a/creme/creme_core/static/creme_core/js/lib/browser.js +++ b/creme/creme_core/static/creme_core/js/lib/browser.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2020-2021 Hybird + Copyright (C) 2020-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -16,7 +16,7 @@ along with this program. If not, see . *******************************************************************************/ -(function($) { +(function() { "use strict"; /* globals BrowserVersion */ @@ -92,4 +92,4 @@ window.BrowserVersion = { } }; -}(jQuery)); +}()); diff --git a/creme/creme_core/static/creme_core/js/lib/color.js b/creme/creme_core/static/creme_core/js/lib/color.js index 9fb053b998..c144d72873 100644 --- a/creme/creme_core/static/creme_core/js/lib/color.js +++ b/creme/creme_core/static/creme_core/js/lib/color.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2009-2023 Hybird + Copyright (C) 2009-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -16,7 +16,7 @@ along with this program. If not, see . *******************************************************************************/ -(function($) { +(function() { "use strict"; /* @@ -48,7 +48,7 @@ window.RGBColor = function(value) { } else if (value instanceof RGBColor) { this.set(value); } else { - this.set($.extend({r: 0, g: 0, b: 0}, value)); + this.set(Object.assign({r: 0, g: 0, b: 0}, value)); } }; @@ -215,4 +215,4 @@ RGBColor.prototype = { } }; -}(jQuery)); +}()); diff --git a/creme/creme_core/static/creme_core/js/lib/faker.js b/creme/creme_core/static/creme_core/js/lib/faker.js index 36f568c044..58fd333307 100644 --- a/creme/creme_core/static/creme_core/js/lib/faker.js +++ b/creme/creme_core/static/creme_core/js/lib/faker.js @@ -16,7 +16,7 @@ along with this program. If not, see . *******************************************************************************/ -(function($) { +(function() { "use strict"; function __import(module, path) { @@ -251,4 +251,4 @@ window.DateFaker.prototype = { } }; -}(jQuery)); +}()); From 9f8baeef19330d0a59913c32476b1ee8176096ca Mon Sep 17 00:00:00 2001 From: joehybird Date: Wed, 15 Jan 2025 14:31:16 +0100 Subject: [PATCH 08/29] Drop fallbacks for widely implemented Object methods --- CHANGELOG.txt | 5 ++ .../creme_core/js/lib/fallbacks/object-0.1.js | 64 +------------------ 2 files changed, 6 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index c3abe3fdde..77406a0974 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -237,6 +237,11 @@ - Array.prototype.ltrim() is removed; Use Array.prototype.trimStart() instead. - Array.prototype.rtrim() is removed; Use Array.prototype.trimEnd() instead. * Drop fallbacks for console methods: log(), warn() & error() + * Drop fallbacks for widely implemented Object methods : + - Object.keys() + - Object.values() + - Object.entries() + - Object.getPrototypeOf() - Apps : * Activities: - Refactor creme.ActivityCalendar : can be now used as independent component. diff --git a/creme/creme_core/static/creme_core/js/lib/fallbacks/object-0.1.js b/creme/creme_core/static/creme_core/js/lib/fallbacks/object-0.1.js index 554c382cd3..df3fbb0138 100644 --- a/creme/creme_core/static/creme_core/js/lib/fallbacks/object-0.1.js +++ b/creme/creme_core/static/creme_core/js/lib/fallbacks/object-0.1.js @@ -26,7 +26,6 @@ } }; - /* istanbul ignore next */ appendStatic('property', function(obj, key, value) { if (value === undefined) { return obj[key]; @@ -36,45 +35,6 @@ return obj; }); - /* istanbul ignore next */ - appendStatic('keys', function(obj, all) { - var keys = []; - - for (var key in obj) { - if (all || obj.hasOwnProperty(key)) { - keys.push(key); - } - } - - return keys; - }); - - /* istanbul ignore next */ - appendStatic('values', function(obj, all) { - var values = []; - - for (var key in obj) { - if (all || obj.hasOwnProperty(key)) { - values.push(obj[key]); - } - } - - return values; - }); - - /* istanbul ignore next */ - appendStatic('entries', function(obj, all) { - var entries = []; - - for (var key in obj) { - if (all || obj.hasOwnProperty(key)) { - entries.push([key, obj[key]]); - } - } - - return entries; - }); - appendStatic('isNone', function(obj) { return obj === undefined || obj === null; }); @@ -107,16 +67,6 @@ return (typeof obj === 'number'); }); - /* - * Was used in converters. Not needed any more. - * - appendStatic('assertIsTypeOf', function(obj, type) { - if (typeof obj !== type) { - throw Error('"' + obj + '" is not a ' + type); - } - }); - */ - appendStatic('isFunc', function(obj) { return (typeof obj === 'function'); }); @@ -132,6 +82,7 @@ } }); + // TODO : Only used in creme.component.Component : move it there ? appendStatic('proxy', function(delegate, context, options) { if (Object.isNone(delegate)) { return; @@ -162,19 +113,6 @@ return proxy; }); - /* istanbul ignore next : compatibility with old IE versions (not really usefull) */ - appendStatic('getPrototypeOf', function(object) { - if (typeof "".__proto__ === 'object') { - return object.__proto__; - } - - if (Object.isNone(object) || object === Object.prototype) { - return null; - } - - return Object.isNone(object.constructor) ? null : object.constructor.prototype; - }); - appendStatic('isSubClassOf', function(object, constructor) { if (constructor && Object.isFunc(constructor.prototype.isPrototypeOf)) { return constructor.prototype.isPrototypeOf(object); From ed409074f0820e82991b58f43082f75faca49ad4 Mon Sep 17 00:00:00 2001 From: Fabre Florian Date: Wed, 15 Jan 2025 16:14:38 +0100 Subject: [PATCH 09/29] Use native Object.assign instead of $.extend for sketch --- CHANGELOG.txt | 1 + creme/sketch/static/sketch/js/bricks.js | 10 +++++----- creme/sketch/static/sketch/js/chart.js | 18 +++++++++--------- creme/sketch/static/sketch/js/color.js | 8 ++++---- creme/sketch/static/sketch/js/draw/drawable.js | 8 ++++---- creme/sketch/static/sketch/js/invert.js | 6 +++--- creme/sketch/static/sketch/js/sketch.js | 12 ++++++------ creme/sketch/static/sketch/js/tooltip.js | 6 +++--- creme/sketch/static/sketch/js/utils.js | 10 +++++----- 9 files changed, 40 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 77406a0974..4d065ca096 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -184,6 +184,7 @@ * Deprecations: - Array.copy(iterable, start, end) is deprecated; Use native code Array.from(iterable).slice(start, end) instead. * Use Object.assign instead of $.extend to remove the dependency to jQuery for : RGBColor, DateFaker, BrowserVersion & Assert. + * Use Object.assign instead of $.extend for sketch components. * FormDialog: - Form submit error responses with HTML can either replace the default overlay content or the frame content. - New creme.dialog.Frame option 'fillOnError'; if enabled the html error response replaces the content. diff --git a/creme/sketch/static/sketch/js/bricks.js b/creme/sketch/static/sketch/js/bricks.js index 812508febc..b8f83d1915 100644 --- a/creme/sketch/static/sketch/js/bricks.js +++ b/creme/sketch/static/sketch/js/bricks.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2022 Hybird + Copyright (C) 2022-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -91,7 +91,7 @@ creme.D3ChartBrickController = creme.component.Component.sub({ brick.getActionBuilders().registerAll({ 'sketch-download': function(url, options, data, e) { - options = $.extend({ + options = Object.assign({ filename: url, width: $(window).innerWidth(), height: $(window).innerHeight() @@ -100,7 +100,7 @@ creme.D3ChartBrickController = creme.component.Component.sub({ return new creme.D3ChartBrickDownloadAction(this._brick, self.chart(), options); }, 'sketch-popover': function(url, options, data, e) { - options = $.extend({ + options = Object.assign({ width: $(window).innerWidth() * 0.8, height: $(window).innerHeight() * 0.8 }, options || {}); @@ -121,7 +121,7 @@ creme.D3ChartBrickDownloadAction = creme.component.Action.sub({ }, _run: function(options) { - options = $.extend({}, this.options(), options || {}); + options = Object.assign({}, this.options(), options || {}); var self = this; @@ -141,7 +141,7 @@ creme.D3ChartBrickPopoverAction = creme.component.Action.sub({ }, _run: function(options) { - options = $.extend({}, this.options(), options || {}); + options = Object.assign({}, this.options(), options || {}); var self = this; diff --git a/creme/sketch/static/sketch/js/chart.js b/creme/sketch/static/sketch/js/chart.js index 6b196db2b1..b9e30518e0 100644 --- a/creme/sketch/static/sketch/js/chart.js +++ b/creme/sketch/static/sketch/js/chart.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2022-2023 Hybird + Copyright (C) 2022-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -25,7 +25,7 @@ creme.D3Chart = creme.component.Component.sub({ _init_: function(options) { options = options || {}; - this._props = $.extend({ + this._props = Object.assign({ drawOnResize: true }, this.defaultProps); @@ -48,10 +48,10 @@ creme.D3Chart = creme.component.Component.sub({ props: function(props) { if (props === undefined) { - return $.extend({}, this._props); + return Object.assign({}, this._props); } - this._props = $.extend(this._props || {}, props); + this._props = Object.assign(this._props || {}, props); return this; }, @@ -139,9 +139,9 @@ creme.D3Chart = creme.component.Component.sub({ saveAs: function(done, filename, options) { var data = this.model() ? this.model().all() : []; - var props = $.extend(this.props(), this.exportProps()); + var props = Object.assign(this.props(), this.exportProps()); - options = $.extend(options || {}, this.exportOptions(data, options, props)); + options = Object.assign(options || {}, this.exportOptions(data, options, props)); this._withShadowSketch(options, function(sketch) { this._export(sketch, data, props); @@ -153,9 +153,9 @@ creme.D3Chart = creme.component.Component.sub({ asImage: function(done, options) { var data = this.model() ? this.model().all() : []; - var props = $.extend(this.props(), this.exportProps()); + var props = Object.assign(this.props(), this.exportProps()); - options = $.extend(options || {}, this.exportOptions(data, options, props)); + options = Object.assign(options || {}, this.exportOptions(data, options, props)); return this._withShadowSketch(options, function(sketch) { this._export(sketch, data, props); @@ -182,7 +182,7 @@ creme.D3Chart = creme.component.Component.sub({ _withShadowSketch: function(options, callable) { Assert.that(this.hasCanvas(), 'D3Chart must have a target sketch to draw on'); - options = $.extend(this.sketch().size(), options || {}); + options = Object.assign(this.sketch().size(), options || {}); var id = _.uniqueId('shadow-d3sketch'); var element = $('
').css({ diff --git a/creme/sketch/static/sketch/js/color.js b/creme/sketch/static/sketch/js/color.js index 482edfa083..2efd73d73e 100644 --- a/creme/sketch/static/sketch/js/color.js +++ b/creme/sketch/static/sketch/js/color.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2023 Hybird + Copyright (C) 2023-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -16,7 +16,7 @@ along with this program. If not, see . *******************************************************************************/ -(function($) { +(function() { "use strict"; creme.d3ColorRange = function(colors, options) { @@ -30,7 +30,7 @@ creme.d3ColorRange = function(colors, options) { }; creme.d3SpectralColors = function(options) { - options = $.extend({ + options = Object.assign({ start: 0, step: 1.0, size: 2 @@ -84,4 +84,4 @@ creme.d3Colorize = function() { return colorize; }; -}(jQuery)); +}()); diff --git a/creme/sketch/static/sketch/js/draw/drawable.js b/creme/sketch/static/sketch/js/draw/drawable.js index f19d05dcb6..9e39bedc9a 100644 --- a/creme/sketch/static/sketch/js/draw/drawable.js +++ b/creme/sketch/static/sketch/js/draw/drawable.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2022-2023 Hybird + Copyright (C) 2022-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -23,7 +23,7 @@ creme.D3Drawable = creme.component.Component.sub({ defaultProps: {}, _init_: function(options) { - this.props($.extend({}, this.defaultProps, options || {})); + this.props(Object.assign({}, this.defaultProps, options || {})); }, drawAll: function(selection) { @@ -40,10 +40,10 @@ creme.D3Drawable = creme.component.Component.sub({ props: function(props) { if (props === undefined) { - return $.extend({}, this._props); + return Object.assign({}, this._props); } - this._props = $.extend(this._props || {}, props); + this._props = Object.assign(this._props || {}, props); return this; }, diff --git a/creme/sketch/static/sketch/js/invert.js b/creme/sketch/static/sketch/js/invert.js index 2cc203c5f5..825718b8bc 100644 --- a/creme/sketch/static/sketch/js/invert.js +++ b/creme/sketch/static/sketch/js/invert.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2023 Hybird + Copyright (C) 2023-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -16,7 +16,7 @@ along with this program. If not, see . *******************************************************************************/ -(function($) { +(function() { "use strict"; creme.d3BisectScale = function(getter) { @@ -51,4 +51,4 @@ creme.d3BisectScale = function(getter) { return invert; }; -}(jQuery)); +}()); diff --git a/creme/sketch/static/sketch/js/sketch.js b/creme/sketch/static/sketch/js/sketch.js index 48466439f0..8478ab035a 100644 --- a/creme/sketch/static/sketch/js/sketch.js +++ b/creme/sketch/static/sketch/js/sketch.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2022-2023 Hybird + Copyright (C) 2022-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -16,12 +16,12 @@ along with this program. If not, see . *******************************************************************************/ -(function($) { +(function() { "use strict"; creme.D3Sketch = creme.component.Component.sub({ _init_: function(options) { - options = $.extend({ + options = Object.assign({ ignoreResize: false }, options || {}); @@ -163,7 +163,7 @@ creme.D3Sketch = creme.component.Component.sub({ saveAs: function(done, filename, options) { // Computed SVG size is set as default for the blob generation because // the attribute value may be '100%' or 'auto' and cause issues - options = $.extend(this.size(), options); + options = Object.assign(this.size(), options); Assert.that(this.isBound(), 'D3Sketch is not bound'); Assert.that(Object.isFunc(done), 'A callback is required to convert and save the SVG.'); @@ -189,7 +189,7 @@ creme.D3Sketch = creme.component.Component.sub({ asImage: function(done, options) { // Computed SVG size is set as default for the blob generation because // the attribute value may be '100%' or 'auto' and cause issues - options = $.extend(this.size(), options); + options = Object.assign(this.size(), options); Assert.that(this.isBound(), 'D3Sketch is not bound'); Assert.that(Object.isFunc(done), 'A callback is required to convert the SVG as image.'); @@ -198,5 +198,5 @@ creme.D3Sketch = creme.component.Component.sub({ } }); -}(jQuery)); +}()); diff --git a/creme/sketch/static/sketch/js/tooltip.js b/creme/sketch/static/sketch/js/tooltip.js index 4bb296da38..12b9f8a528 100644 --- a/creme/sketch/static/sketch/js/tooltip.js +++ b/creme/sketch/static/sketch/js/tooltip.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2023 Hybird + Copyright (C) 2023-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -18,7 +18,7 @@ /* globals DOMPoint */ -(function($) { +(function() { "use strict"; function bboxAnchorPoint(target, node, direction) { @@ -217,4 +217,4 @@ creme.d3Tooltip = function(root) { return tooltip; }; -}(jQuery)); +}()); diff --git a/creme/sketch/static/sketch/js/utils.js b/creme/sketch/static/sketch/js/utils.js index 614766848e..3629de5d87 100644 --- a/creme/sketch/static/sketch/js/utils.js +++ b/creme/sketch/static/sketch/js/utils.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2022-2024 Hybird + Copyright (C) 2022-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -16,7 +16,7 @@ along with this program. If not, see . *******************************************************************************/ -(function($) { +(function() { "use strict"; function Transform() { @@ -132,7 +132,7 @@ creme.svgAsXml = function(svg, options) { return [ '', - $(svg).html(), + svg.innerHTML, '' ].join('\n').template({ width: options.width || (svg.getAttribute('width') || 'auto'), @@ -156,7 +156,7 @@ creme.svgAsDataURI = function(svg, options) { }; creme.svgAsBlob = function(done, svg, options) { - options = $.extend({ + options = Object.assign({ encoderType: 'image/svg+xml', encoderQuality: 0.8 }, options || {}); @@ -390,4 +390,4 @@ creme.d3PreventResizeObserverLoop = function(callback) { }; }; -}(jQuery)); +}()); From fd68cacf416782f26899b5d5f936abd757248479 Mon Sep 17 00:00:00 2001 From: joehybird Date: Thu, 16 Jan 2025 06:51:52 +0100 Subject: [PATCH 10/29] Improve d3.radialAxis coverage --- .../static/sketch/js/lib/d3-radial-axis.js | 8 +++-- .../static/sketch/js/tests/d3-radial-axis.js | 30 ++++++++++++++++++- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/creme/sketch/static/sketch/js/lib/d3-radial-axis.js b/creme/sketch/static/sketch/js/lib/d3-radial-axis.js index fa5dbfea4f..85b6b0f7d0 100644 --- a/creme/sketch/static/sketch/js/lib/d3-radial-axis.js +++ b/creme/sketch/static/sketch/js/lib/d3-radial-axis.js @@ -116,14 +116,16 @@ function radialAxis(scale, radius, outer) { tickExit = tickExit.transition(context) .attr("opacity", epsilon) .attr("transform", function(d) { - return isFinite(d = anglePos(d)) ? angularTranslate(d + offset, radius) : this.getAttribute("transform"); + var pos = anglePos(d); + return isFinite(pos) ? angularTranslate(pos + offset, radius) : this.getAttribute("transform"); }); tickEnter.attr("opacity", epsilon) .attr("transform", function(d) { var p = this.parentNode.__axis; - var next = p && isFinite(p = p(d + offset) ? p : anglePos(d), radius); - return angularTranslate(next); + var pos = p ? p(d) : NaN; + pos = isFinite(pos) ? pos : anglePos(d); + return angularTranslate(pos + offset, radius); }); } diff --git a/creme/sketch/static/sketch/js/tests/d3-radial-axis.js b/creme/sketch/static/sketch/js/tests/d3-radial-axis.js index e0b8629fd1..9a4a88532d 100644 --- a/creme/sketch/static/sketch/js/tests/d3-radial-axis.js +++ b/creme/sketch/static/sketch/js/tests/d3-radial-axis.js @@ -73,7 +73,7 @@ QUnit.test('d3.axisRadial (set props)', function(assert) { equal(axis.offset(), 1); }); -QUnit.test('d3.axisRadialInner', function(assert) { +QUnit.test('d3.axisRadialInner (linear scale)', function(assert) { var scale = d3.scaleLinear() .domain([0, 100]) .range([_.toRadian(-180), _.toRadian(180)]); @@ -100,6 +100,34 @@ QUnit.test('d3.axisRadialInner', function(assert) { }); }); +QUnit.test('d3.axisRadialInner (ordinal scale)', function(assert) { + var scale = d3.scaleBand() + .domain([0, 25, 50, 75, 100]) + .range([_.toRadian(-180), _.toRadian(180)], 0.1) + .padding(0.1); + + var axis = d3.axisRadialInner(scale, 10) + .tickFormat(function(d) { return 'V' + d; }) + .tickPadding(0); + + var output = d3.select(document.createElement('g')); + + output.call(axis); + + this.assertD3Nodes(output, { + '.tick text': 5, /* V0 V25 V25 V75 V100 */ + '.domain': 1 + }); + + axis.tickValues([0, 50, 100]); + output.call(axis); + + this.assertD3Nodes(output, { + '.tick text': 3, /* V0 V50 V100 */ + '.domain': 1 + }); +}); + QUnit.test('d3.axisRadialOuter', function(assert) { var scale = d3.scaleLinear() .domain([0, 100]) From 8dae994389b1d5407ff6bea900ff18898a2e7716 Mon Sep 17 00:00:00 2001 From: Fabre Florian Date: Thu, 16 Jan 2025 15:14:44 +0100 Subject: [PATCH 11/29] Improve creme.d3Colorize coverage. Add support for CSS names in RGBColor class --- .../static/creme_core/js/lib/color.js | 53 ++++++++++---- .../static/creme_core/js/tests/color.js | 18 +++++ creme/settings.py | 1 + creme/sketch/static/sketch/js/color.js | 19 +++-- creme/sketch/static/sketch/js/tests/color.js | 71 +++++++++++++++++++ 5 files changed, 144 insertions(+), 18 deletions(-) create mode 100644 creme/sketch/static/sketch/js/tests/color.js diff --git a/creme/creme_core/static/creme_core/js/lib/color.js b/creme/creme_core/static/creme_core/js/lib/color.js index c144d72873..3eb3f271a5 100644 --- a/creme/creme_core/static/creme_core/js/lib/color.js +++ b/creme/creme_core/static/creme_core/js/lib/color.js @@ -19,27 +19,56 @@ (function() { "use strict"; -/* -var absround = function(value) { - return (0.5 + value) << 0; +var __namedColors = { + black: "#000000", + silver: "#c0c0c0", + gray: "#808080", + white: "#ffffff", + maroon: "#800000", + red: "#ff0000", + purple: "#800080", + fuchsia: "#ff00ff", + green: "#008000", + lime: "#00ff00", + olive: "#808000", + yellow: "#ffff00", + navy: "#000080", + blue: "#0000ff", + teal: "#008080", + aqua: "#00ffff", + orange: "#ffa500" }; -var scaleround = function(value, precision) { - var scale = Math.pow(10, precision || 0); - return Math.round(value * scale) / scale; -}; +function parseCSSColorName(value, cached) { + var hex = __namedColors[value]; -var clamp = function(value, min, max) { - return Math.max(min, Math.min(max, value)); -}; -*/ + if (hex === undefined) { + var ctx = document.createElement('canvas').getContext('2d'); + ctx.fillStyle = value; + hex = ctx.fillStyle; + + if (hex === '#000000' && value.toLowerCase() !== 'black') { + throw new Error('"${0}" is not a valid css named color'.template([value])); + } + + __namedColors[value] = hex; + } + + return hex; +} window.RGBColor = function(value) { if (Object.isString(value)) { + value = value.toLowerCase(); + if (value.startsWith('#')) { this.hex(value); - } else { + } else if (value.startsWith('rgb(')) { this.rgb(value); + } else if (value.match(/[a-z]+$/)) { + this.hex(parseCSSColorName(value)); + } else { + throw new Error('"${0}" is not a RGB css value'.template([value])); } } else if (isFinite(value)) { this.decimal(value); diff --git a/creme/creme_core/static/creme_core/js/tests/color.js b/creme/creme_core/static/creme_core/js/tests/color.js index bb7c100b41..4dd757571f 100644 --- a/creme/creme_core/static/creme_core/js/tests/color.js +++ b/creme/creme_core/static/creme_core/js/tests/color.js @@ -81,6 +81,9 @@ QUnit.test('color.RGBColor (rgb)', function(assert) { equal('#FF12FD', new RGBColor([255, 18, 253]).toString()); equal('#FF12FD', new RGBColor('rgb(255, 18, 253)').toString()); + equal('rgb(255,18,253)', new RGBColor('#FF12FD').rgb()); + equal('rgb(0,18,0)', new RGBColor({g: 0x12, b: 0}).rgb()); + this.assertRaises(function() { return new RGBColor().rgb('#aa12fd'); }, Error, 'Error: "#aa12fd" is not a RGB css value'); @@ -88,6 +91,21 @@ QUnit.test('color.RGBColor (rgb)', function(assert) { this.assertRaises(function() { return new RGBColor().rgb('hls(1.0, 0.0, 0.0)'); }, Error, 'Error: "hls(1.0, 0.0, 0.0)" is not a RGB css value'); + + this.assertRaises(function() { + return new RGBColor().rgb(12); + }, Error, 'Error: "12" is not a RGB css value'); +}); + +QUnit.test('color.RGBColor (css name)', function(assert) { + equal('#000000', new RGBColor('black').toString()); + + this.assertRaises(function() { + return new RGBColor('unknown'); + }, Error, 'Error: "unknown" is not a valid css named color'); + + equal('#FFFF00', new RGBColor('yellow').toString()); + equal('#FFD700', new RGBColor('gold').toString()); }); QUnit.test('color.RGBColor (set / clone)', function(assert) { diff --git a/creme/settings.py b/creme/settings.py index 8efb10ccf6..3d61115055 100644 --- a/creme/settings.py +++ b/creme/settings.py @@ -1162,6 +1162,7 @@ ('creme.sketch', 'sketch/js/tests/bricks.js'), ('creme.sketch', 'sketch/js/tests/demo.js'), ('creme.sketch', 'sketch/js/tests/invert.js'), + ('creme.sketch', 'sketch/js/tests/color.js'), ('creme.crudity', 'crudity/js/tests/crudity-actions.js'), ('creme.cti', 'cti/js/tests/cti-actions.js'), ('creme.emails', 'emails/js/tests/emails-actions.js'), diff --git a/creme/sketch/static/sketch/js/color.js b/creme/sketch/static/sketch/js/color.js index 2efd73d73e..04f80353dd 100644 --- a/creme/sketch/static/sketch/js/color.js +++ b/creme/sketch/static/sketch/js/color.js @@ -43,22 +43,24 @@ creme.d3SpectralColors = function(options) { creme.d3Colorize = function() { var props = { - scale: function(d) { return 'black'; } + scale: function(d) { return 'black'; }, + accessor: function(d) { return d.x; } }; function colorize(data) { - return data.map(function(d) { + return data.map(function(d, i) { var color = props.color ? props.color(d) : d.color; var textColor = props.textColor ? props.textColor(d) : d.textColor; + var value = props.accessor ? props.accessor(d, i) : d; - d.color = color || props.scale(d.x); + d.color = color || props.scale(value); + + var rgbColor = new RGBColor(d.color); + d.isDarkColor = rgbColor.isDark(); if (textColor) { d.textColor = textColor; } else { - var rgbColor = new RGBColor(d.color); - - d.isDarkColor = rgbColor.isDark(); d.textColor = d.isDarkColor ? 'white' : 'black'; } @@ -81,6 +83,11 @@ creme.d3Colorize = function() { return colorize; }; + colorize.accessor = function(accessor) { + props.accessor = accessor; + return colorize; + }; + return colorize; }; diff --git a/creme/sketch/static/sketch/js/tests/color.js b/creme/sketch/static/sketch/js/tests/color.js new file mode 100644 index 0000000000..c297015bc0 --- /dev/null +++ b/creme/sketch/static/sketch/js/tests/color.js @@ -0,0 +1,71 @@ +(function($) { + +QUnit.module("creme.sketch.color", new QUnitMixin()); + +QUnit.test('creme.d3Colorize', function(assert) { + var data = [{text: 'A'}, {text: 'B'}, {text: 'C'}]; + var colors = ["#000000", "#cccccc", "#ffffff"]; + var scale = d3.scaleOrdinal() + .domain([0, 1, 2]) + .range(colors); + + var colorize = creme.d3Colorize() + .scale(scale) + .accessor(function(d, i) { return i; }); + + deepEqual(colorize(data), [ + {text: 'A', textColor: 'white', color: '#000000', isDarkColor: true}, + {text: 'B', textColor: 'black', color: '#cccccc', isDarkColor: false}, + {text: 'C', textColor: 'black', color: '#ffffff', isDarkColor: false} + ]); +}); + +QUnit.test('creme.d3Colorize (default accessor)', function(assert) { + var data = [{text: 'A', x: 2}, {text: 'B', x: 1}, {text: 'C', x: 0}]; + var colors = ["#000000", "#cccccc", "#ffffff"]; + var scale = d3.scaleOrdinal() + .domain([0, 1, 2]) + .range(colors); + + var colorize = creme.d3Colorize() + .scale(scale); + + deepEqual(colorize(data), [ + {text: 'A', x: 2, textColor: 'black', color: '#ffffff', isDarkColor: false}, + {text: 'B', x: 1, textColor: 'black', color: '#cccccc', isDarkColor: false}, + {text: 'C', x: 0, textColor: 'white', color: '#000000', isDarkColor: true} + ]); +}); + +QUnit.test('creme.d3Colorize (default scale)', function(assert) { + var data = [{text: 'A', x: 2}, {text: 'B', x: 1}, {text: 'C', x: 0}]; + var colorize = creme.d3Colorize(); + + deepEqual(colorize(data), [ + {text: 'A', x: 2, textColor: 'white', color: 'black', isDarkColor: true}, + {text: 'B', x: 1, textColor: 'white', color: 'black', isDarkColor: true}, + {text: 'C', x: 0, textColor: 'white', color: 'black', isDarkColor: true} + ]); +}); + +QUnit.test('creme.d3Colorize (data.color)', function(assert) { + var data = [{text: 'A', x: 2, color: '#aa0000'}, {text: 'B', x: 1, color: 'yellow'}, {text: 'C', x: 0, color: 'gray'}]; + var scale = d3.scaleOrdinal() + .domain([0, 1, 2]) + .range(["#000000", "#cccccc", "#ffffff"]); + var textScale = d3.scaleOrdinal() + .domain([0, 1, 2]) + .range(["#ff0000", "#00ff00", "#0000ff"]); + + var colorize = creme.d3Colorize() + .scale(scale) + .textColor(textScale); + + deepEqual(colorize(data), [ + {text: 'A', x: 2, textColor: '#ff0000', color: '#aa0000', isDarkColor: true}, + {text: 'B', x: 1, textColor: '#00ff00', color: 'yellow', isDarkColor: false}, + {text: 'C', x: 0, textColor: '#0000ff', color: 'gray', isDarkColor: true} + ]); +}); + +}(jQuery)); From 9bf9c5e890de857e1131904c10ab5198b47376e5 Mon Sep 17 00:00:00 2001 From: Fabre Florian Date: Fri, 17 Jan 2025 16:42:20 +0100 Subject: [PATCH 12/29] Fix DateFaker issues --- creme/creme_core/static/creme_core/js/lib/faker.js | 14 ++++++++++++-- .../creme_core/static/creme_core/js/tests/faker.js | 8 ++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/creme/creme_core/static/creme_core/js/lib/faker.js b/creme/creme_core/static/creme_core/js/lib/faker.js index 58fd333307..67b08a63c4 100644 --- a/creme/creme_core/static/creme_core/js/lib/faker.js +++ b/creme/creme_core/static/creme_core/js/lib/faker.js @@ -234,14 +234,24 @@ window.DateFaker.prototype = { var frozen = this.frozen; try { - window.Date = function() { - return new NativeDate(frozen); + window.Date = function(value) { + if (arguments.length > 1) { + // This hack allows to call new Date with an array of arguments. + // It is really tricky but do the job since ECMAScript 5+ + var D = NativeDate.bind.apply(NativeDate, [null].concat(Array.from(arguments))); + return new D(); + } + + return new NativeDate(value || frozen); }; window.Date.now = function() { return new NativeDate(frozen); }; + window.Date.parse = NativeDate.parse; + window.Date.UTC = NativeDate.UTC; + callable(this); } finally { window.Date = NativeDate; diff --git a/creme/creme_core/static/creme_core/js/tests/faker.js b/creme/creme_core/static/creme_core/js/tests/faker.js index 528baa44c7..5b8efc7e9b 100644 --- a/creme/creme_core/static/creme_core/js/tests/faker.js +++ b/creme/creme_core/static/creme_core/js/tests/faker.js @@ -479,6 +479,14 @@ QUnit.parametrize('DateFaker.with', [ deepEqual(faker, datefaker); equal(expected, new Date().toISOString()); equal(expected, Date.now().toISOString()); + + // constructor should works as usual + equal('2020-12-31T00:08:30.000Z', new Date('2020-12-31T00:08:30+00:00').toISOString()); + equal(new Date('2020-12-31T00:08:30').toISOString(), new Date(2020, 11, 31, 0, 8, 30, 0).toISOString()); + + // same for the static methods + equal(1609373310000, Date.UTC(2020, 11, 31, 0, 8, 30, 0)); + equal(1609373310000, Date.parse('2020-12-31T00:08:30+00:00')); }); equal(origin, window.Date); From c6ee55a428b3347b76135b727bacb06a9d71df91 Mon Sep 17 00:00:00 2001 From: Fabre Florian Date: Tue, 21 Jan 2025 11:48:14 +0100 Subject: [PATCH 13/29] Drop creme.widget.template(); Use String.prototype.template() instead --- CHANGELOG.txt | 1 + creme/creme_core/static/creme_core/js/tests/widgets/base.js | 4 ++-- creme/creme_core/static/creme_core/js/widgets/base.js | 2 ++ .../creme_core/static/creme_core/js/widgets/entityselector.js | 4 ++-- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 4d065ca096..d7480de94b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -243,6 +243,7 @@ - Object.values() - Object.entries() - Object.getPrototypeOf() + * Drop creme.widget.template(); Use String.prototype.template instead. - Apps : * Activities: - Refactor creme.ActivityCalendar : can be now used as independent component. diff --git a/creme/creme_core/static/creme_core/js/tests/widgets/base.js b/creme/creme_core/static/creme_core/js/tests/widgets/base.js index 8d630aa0ed..1acc33faba 100644 --- a/creme/creme_core/static/creme_core/js/tests/widgets/base.js +++ b/creme/creme_core/static/creme_core/js/tests/widgets/base.js @@ -45,7 +45,7 @@ QUnit.test('creme.widget.parseattr (with exclusion)', function(assert) { attrs = creme.widget.parseattr($('
'), ['attr1', 'attr3']); deepEqual(attrs, {attr2: 'val2'}); }); - +/* QUnit.test('creme.widget.template (template:no keys)', function(assert) { var result = creme.widget.template(''); equal(result, ''); @@ -84,7 +84,7 @@ QUnit.test('creme.widget.template (template:keys, values: not in template)', fun result = creme.widget.template('template with key1=${key1} and key2=${key2}, ${key1}', {key1: 'value1', key3: 'value3'}); equal(result, 'template with key1=value1 and key2=${key2}, value1'); }); - +*/ QUnit.test('creme.widget.parseval (parser: json, value: none)', function(assert) { var result = creme.widget.parseval(undefined, JSON.parse); equal(result, undefined); diff --git a/creme/creme_core/static/creme_core/js/widgets/base.js b/creme/creme_core/static/creme_core/js/widgets/base.js index 7508375db6..a2917f912f 100644 --- a/creme/creme_core/static/creme_core/js/widgets/base.js +++ b/creme/creme_core/static/creme_core/js/widgets/base.js @@ -250,6 +250,7 @@ $.extend(creme.widget, { }, // TODO : remove it and replace it by creme.utils.template or String.template + /* template: function(template, values) { if (template === undefined || values === undefined) { return template; @@ -282,6 +283,7 @@ $.extend(creme.widget, { return res; }, + */ parseattr: function(element, excludes) { var attributes = {}; diff --git a/creme/creme_core/static/creme_core/js/widgets/entityselector.js b/creme/creme_core/static/creme_core/js/widgets/entityselector.js index 306e5aa7d8..3d545373f9 100644 --- a/creme/creme_core/static/creme_core/js/widgets/entityselector.js +++ b/creme/creme_core/static/creme_core/js/widgets/entityselector.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2009-2022 Hybird + Copyright (C) 2009-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -128,7 +128,7 @@ creme.widget.EntitySelector = creme.widget.declare('ui-creme-entityselector', { return; } - var url = creme.widget.template(options.labelURL, {'id': value}); + var url = (options.labelURL || '').template({id: value}); var default_label = gettext('Entity #%s (not viewable)').format(value); this._backend.get(url, {fields: ['summary']}, From b92b1c43190a9fc694223ad136536fdc1821f799 Mon Sep 17 00:00:00 2001 From: Fabre Florian Date: Tue, 21 Jan 2025 11:03:00 +0100 Subject: [PATCH 14/29] Improve NotificationBox & add some unit testing to it. --- CHANGELOG.txt | 4 + .../chantilly/creme_core/css/header_menu.css | 2 +- .../static/creme_core/js/notification.js | 259 +++++++----- .../creme_core/js/tests/views/notification.js | 385 ++++++++++++++++++ .../icecream/creme_core/css/header_menu.css | 2 +- .../creme_core/header/menu-base.html | 8 +- creme/settings.py | 1 + 7 files changed, 551 insertions(+), 110 deletions(-) create mode 100644 creme/creme_core/static/creme_core/js/tests/views/notification.js diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d7480de94b..762baa55a3 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -327,6 +327,10 @@ (good news, some {% block %} have been added) : - "creme_core/bricks/base/hat-card.html" - "creme_core/bricks/generic/hat-bar.html" + # Javascript : + - Refactor creme.notification.NotificationBox & add unit tests + - Toggle fetch 'job' when disabling a tab or window. + - Elapsed time from message creation is now updated each minute. # Apps : * Creme_config : - In the brick 'GenericModelBrick', "meta" is not injected in the context anymore. diff --git a/creme/creme_core/static/chantilly/creme_core/css/header_menu.css b/creme/creme_core/static/chantilly/creme_core/css/header_menu.css index fb14fbb8ce..204d6beaa6 100644 --- a/creme/creme_core/static/chantilly/creme_core/css/header_menu.css +++ b/creme/creme_core/static/chantilly/creme_core/css/header_menu.css @@ -413,7 +413,7 @@ span.create-group-entry { box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5); } -.notification-box.notification-box-activated .notification-panel { +.notification-box:hover .notification-panel { visibility: visible; opacity: 1; } diff --git a/creme/creme_core/static/creme_core/js/notification.js b/creme/creme_core/static/creme_core/js/notification.js index b4ffeec8f8..c4c294ff9f 100644 --- a/creme/creme_core/static/creme_core/js/notification.js +++ b/creme/creme_core/static/creme_core/js/notification.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2024 Hybird + Copyright (C) 2024-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -27,55 +27,57 @@ creme.notification = {}; /* TODO: unit tests */ -/* globals setInterval creme_media_url */ + +/* globals clearInterval setInterval creme_media_url */ creme.notification.NotificationBox = creme.component.Component.sub({ - _init_: function(options) { - options = $.extend({ - refreshDelay: 300000 // In milliseconds. Default 5 minutes. + _init_: function(element, options) { + options = Object.assign({ + refreshDelay: 300000, // In milliseconds. Default 5 minutes. + deltaRefreshDelay: 60000, // In milliseconds. Default 1 minutes. + refreshUrl: '', + discardUrl: '' }, options || {}); - this._refreshDelay = options.refreshDelay; + Assert.not(Object.isEmpty(options.refreshUrl), 'refreshUrl is required'); + Assert.not(Object.isEmpty(options.discardUrl), 'discardUrl is required'); - this._initialDataSelector = options.initialDataSelector; - if (Object.isEmpty(this._initialDataSelector)) { - throw new Error('initialDataSelector is required'); - } + Assert.not(element.is('.is-active'), 'NotificationBox is already active'); + this._element = element; + this._refreshDelay = options.refreshDelay; + this._deltaRefreshDelay = options.deltaRefreshDelay; + this._initialDataSelector = options.initialDataSelector; this._refreshUrl = options.refreshUrl; - if (Object.isEmpty(this._refreshUrl)) { - throw new Error('refreshUrl is required'); - } - this._discardUrl = options.discardUrl; - if (Object.isEmpty(this._discardUrl)) { - throw new Error('discardUrl is required'); - } - this._count = 0; - this._overlay = new creme.dialog.Overlay(); + this.setup(element, options); }, - isBound: function() { - return Object.isNone(this._element) === false; + isFetchActive: function() { + return Boolean(this._fetchJob); }, - bind: function(element) { - if (this.isBound()) { - throw new Error('NotificationBox is already bound'); - } + isPaused: function() { + return document.hidden; + }, - this._element = element; + initialData: function() { + var script = this._element.find('script[type$="/json"].notification-box-data:first'); + var data = creme.utils.JSON.readScriptText(script); + + return Object.assign({ + count: 0, + notifications: [] + }, Object.isEmpty(data) ? {} : JSON.parse(data)); + }, - this._updateBox( - JSON.parse(creme.utils.JSON.readScriptText(element.find(this._initialDataSelector))) - ); + setup: function(element, options) { + var self = this; - // Activate panel on hover events - element.on('mouseenter', function(e) { - $(this).addClass('notification-box-activated'); - }).on('mouseleave', function(e) { - $(this).removeClass('notification-box-activated'); - }); + this._count = 0; + this._overlay = new creme.dialog.Overlay(); + + this._updateBox(this.initialData()); // NB: we attach to
    & not the parent
    because the overlay sets // the position as "relative" (which breaks our layout). @@ -90,12 +92,56 @@ creme.notification.NotificationBox = creme.component.Component.sub({ }) ); - // TODO: <() => this._refresh()> with more modern JS - setInterval(this._refresh.bind(this), this._refreshDelay); + element.on('click', '.discard-notification', function(e) { + e.preventDefault(); + self._onDiscardItem($(this).parents('.notification-item:first')); + }); + + this.startFetch(); + element.addClass('is-active'); return this; }, + startFetch: function() { + if (!this.isFetchActive()) { + this._fetchJob = setInterval(this._fetchItems.bind(this), this._refreshDelay); + this._timeDeltaJob = setInterval(this._updateDeltas.bind(this), this._deltaRefreshDelay); + } + + return this; + }, + + stopFetch: function() { + if (!Object.isNone(this._fetchJob)) { + clearInterval(this._fetchJob); + this._fetchJob = null; + } + + if (!Object.isNone(this._timeDeltaJob)) { + clearInterval(this._timeDeltaJob); + this._timeDeltaJob = null; + } + + return this; + }, + + _onDiscardItem: function(item) { + var self = this; + var id = item.data('id'); + + if (!Object.isEmpty(id)) { + creme.utils.ajaxQuery( + this._discardUrl, + {action: 'post', warnOnFail: true}, + {id: id} + ).onDone(function() { + item.remove(); + self._updateCounter(self._count - 1); + }).start(); + } + }, + _humanizedTimeDelta: function(secondsTimedelta) { var minutesTimeDelta = Math.round(secondsTimedelta / 60); @@ -126,55 +172,56 @@ creme.notification.NotificationBox = creme.component.Component.sub({ countWidget.toggleClass('is-empty', !count); }, + _updateDeltas: function() { + if (this.isPaused()) { + return; + } + + var now = Date.now(); + + $('.notification-item').each(function() { + var item = $(this); + var created = item.data('created'); + var label = this._humanizedTimeDelta(Math.round((now - created) / 1000)); + + item.find('.notification-created').text(label); + }.bind(this)); + }, + _updateItems: function(notifications) { var element = this._element; - var discardUrl = this._discardUrl; - var itemsWidget = element.find('.notification-items'); - itemsWidget.empty(); - - var now_ts = Date.now(); - var box = this; - notifications.map(function(itemData) { - var notif_id = itemData.id; - var button = $( - ''.template( - {label: gettext('Validate')} - ) - ).on('click', function(e) { - // e.preventDefault(); - creme.utils.ajaxQuery( - discardUrl, - {action: 'post', warnOnFail: true}, - {id: notif_id} - ).onDone(function() { - element.find('[data-notification-id="${id}"]'.template({id: notif_id})).remove(); - box._updateCounter(box._count - 1); - }).start(); + var items = element.find('.notification-items'); + + var now = Date.now(); + + var html = notifications.map(function(itemData) { + var created = Date.parse(itemData.created); + var createdLabel = new Date(created).toLocaleString(); + + return ( + '
  • ' + + '${channel}' + + '${subject}' + + '${timeDeltaLabel}' + + '
    ${body}
    ' + + '' + + '
  • ' + ).template({ + id: itemData.id, + level: itemData.level, + channel: itemData.channel, + created: created, + createdLabel: createdLabel, + // TODO: update dynamically this label every minute + // TODO: use momentjs for this job + timeDeltaLabel: this._humanizedTimeDelta(Math.round((now - created) / 1000)), + subject: itemData.subject, + body: itemData.body, + discardLabel: gettext('Validate') }); + }.bind(this)).join(''); - var created_ts = Date.parse(itemData.created); - var created = new Date(created_ts); - var item = $( - ( - '
  • ' + - '${channel}' + - '${subject}' + - '${humanized_created}' + - '
    ${body}
    ' + - '
  • ' - ).template({ - id: notif_id, - level: itemData.level, - channel: itemData.channel, - created: created.toLocaleString(), - // TODO: update dynamically this label every minute - humanized_created: this._humanizedTimeDelta(Math.round((now_ts - created_ts) / 1000)), - subject: itemData.subject, - body: itemData.body - })).append(button); - - itemsWidget.append(item); - }.bind(this)); + items.html(html); }, _updateBox: function(data) { @@ -188,7 +235,7 @@ creme.notification.NotificationBox = creme.component.Component.sub({ container.find('span').text(message || ''); }, - _refresh: function() { + _fetchItems: function() { /* TODO: our script will continue to be called even if the tab is not visible (at least on PS -- it seems iOS does not wake up not visible tabs). Here we do not query the server when the tab is not visible, so we avoid @@ -197,36 +244,40 @@ creme.notification.NotificationBox = creme.component.Component.sub({ query the server every time 'refreshDelay' milliseconds have been spend in visible mode)? Hint: see + Note : this event is "dangerous" because it appears many even without any need. + Skipping an fetch each 5 minutes is way more efficient. */ - if (document.hidden) { + if (this.isPaused()) { return; } + var self = this; var overlay = this._overlay; + overlay.visible(true); - creme.ajax.query( + this._fetchQuery = creme.ajax.query( this._refreshUrl, {backend: {sync: false, dataType: 'json'}} - ).onDone( - function(event, data) { - this._updateBox(data); - this._updateErrorMessage(); - }.bind(this) - ).onFail( - function(event, data, error) { - /* E.g. - - event === fail - - data === undefined - - error === {type: 'request', status: 0, request: {…}, message: 'HTTP 0 - error'} - */ - this._updateErrorMessage( - gettext('An error happened when retrieving notifications (%s)').format(error.message) - ); - }.bind(this) - ).onComplete( - function() { overlay.visible(false); } - ).start(); + ).onDone(function(event, data) { + self._updateBox(data); + self._updateErrorMessage(''); + }).onFail(function(event, data, error) { + /* E.g. + - event === fail + - data === undefined + - error === {type: 'request', status: 0, request: {…}, message: 'HTTP 0 - error'} + */ + self._updateErrorMessage( + gettext('An error happened when retrieving notifications (%s)').format(error.message) + ); + }).onComplete(function() { + overlay.visible(false); + }).start(); } }); +creme.setupNotificationBox = function(element, options) { + return new creme.notification.NotificationBox($(element), options); +}; + }(jQuery)); diff --git a/creme/creme_core/static/creme_core/js/tests/views/notification.js b/creme/creme_core/static/creme_core/js/tests/views/notification.js new file mode 100644 index 0000000000..89d1532b60 --- /dev/null +++ b/creme/creme_core/static/creme_core/js/tests/views/notification.js @@ -0,0 +1,385 @@ +/* globals FunctionFaker PropertyFaker */ + +(function($) { +"use strict"; + +QUnit.module("creme.NotificationBox", new QUnitMixin(QUnitEventMixin, + QUnitAjaxMixin, + QUnitDialogMixin, { + beforeEach: function() { + var self = this; + var backend = this.backend; + backend.options.enableUriSearch = true; + + this.setMockBackendGET({ + 'mock/notifs/refresh': function() { + return backend.response(200, JSON.stringify(self.defaultNotificationData())); + }, + 'mock/notifs/refresh/fail': backend.response(400, ''), + 'mock/notifs/all': backend.response(200, '') + }); + + this.setMockBackendPOST({ + 'mock/notifs/discard': backend.response(200, ''), + 'mock/notifs/discard/fail': backend.response(400, '') + }); + }, + + afterEach: function() { + $('.glasspane').detach(); + }, + + defaultNotificationData: function() { + return { + count: 3, + notifications: [ + { + id: 1, + created: '2025-01-15T16:30:00', + level: '1', + channel: 'A', + subject: 'Subject #1', + body: 'Content #1' + }, + { + id: 2, + created: '2025-01-16T08:35:00', + level: '2', + channel: 'A', + subject: 'Subject #2', + body: 'Content #2' + }, + { + id: 3, + created: '2025-01-16T17:12:00', + level: '2', + channel: 'B', + subject: 'Subject #3', + body: 'Content #3' + } + ] + }; + }, + + createNotificationBoxHtml: function(options) { + options = Object.assign({ + props: {}, + initialData: { + count: 0 + } + }, options || {}); + + return ( + '
    ' + + '' + + '' + + 'Notifications' + + '
    ' + + '
    ??
    ' + + '
      ' + + '' + + '
      ' + + '
      ' + ).template({ + data: JSON.stringify(options.initialData) + }); + } +})); + +QUnit.test('creme.NotificationBox', function() { + var element = $(this.createNotificationBoxHtml()).appendTo(this.qunitFixture()); + + equal(element.is('.is-active'), false); + + var box = new creme.notification.NotificationBox(element, { + refreshUrl: 'mock/notifs/refresh', + discardUrl: 'mock/notifs/discard' + }).stopFetch(); + + equal(element.is('.is-active'), true); + deepEqual(box._element, element); + deepEqual(box._refreshUrl, 'mock/notifs/refresh'); + deepEqual(box._discardUrl, 'mock/notifs/discard'); + deepEqual(box.initialData(), { + count: 0, + notifications: [] + }); +}); + +QUnit.test('creme.NotificationBox (invalid urls)', function() { + var element = $(this.createNotificationBoxHtml({ + initialData: this.defaultNotificationData() + })).appendTo(this.qunitFixture()); + + this.assertRaises(function() { + return new creme.notification.NotificationBox(element, {}); + }); + + this.assertRaises(function() { + return new creme.notification.NotificationBox(element, { + refreshUrl: 'mock/notifs/refresh' + }); + }); + + this.assertRaises(function() { + return new creme.notification.NotificationBox(element, { + discardUrl: 'mock/notifs/discard' + }); + }); +}); + +QUnit.test('creme.NotificationBox (already active)', function() { + var element = $(this.createNotificationBoxHtml({ + initialData: this.defaultNotificationData() + })).appendTo(this.qunitFixture()); + + element.addClass('is-active'); + + this.assertRaises(function() { + return new creme.notification.NotificationBox(element, { + refreshUrl: 'mock/notifs/refresh', + discardUrl: 'mock/notids/all' + }); + }, Error, 'Error: NotificationBox is already active'); +}); + +QUnit.test('creme.NotificationBox (initialData)', function() { + var element = $(this.createNotificationBoxHtml({ + initialData: this.defaultNotificationData() + })).appendTo(this.qunitFixture()); + + this.withFrozenTime('2025-01-16T17:30:00', function() { + var box = new creme.notification.NotificationBox(element, { + refreshUrl: 'mock/notifs/refresh', + discardUrl: 'mock/notids/all' + }).stopFetch(); + + deepEqual(box.initialData(), this.defaultNotificationData()); + + var counter = element.find('.notification-box-count'); + + equal(counter.text(), '3'); + equal(counter.is('.is-empty'), false); + + this.equalHtml(( + '
    • ' + + 'A' + + 'Subject #1' + + '${deltaLabelA}' + + '
      Content #1
      ' + + '' + + '
    • ' + + '
    • ' + + 'A' + + 'Subject #2' + + '${deltaLabelB}' + + '
      Content #2
      ' + + '' + + '
    • ' + + '
    • ' + + 'B' + + 'Subject #3' + + '${deltaLabelC}' + + '
      Content #3
      ' + + '' + + '
    • ' + ).template({ + discardLabel: gettext('Validate'), + timestampA: Date.parse('2025-01-15T16:30:00'), + timestampB: Date.parse('2025-01-16T08:35:00'), + timestampC: Date.parse('2025-01-16T17:12:00'), + createdTitleA: new Date('2025-01-15T16:30:00').toLocaleString(), + createdTitleB: new Date('2025-01-16T08:35:00').toLocaleString(), + createdTitleC: new Date('2025-01-16T17:12:00').toLocaleString(), + deltaLabelA: ngettext('More than %d day ago', 'More than %d days ago', 1).format(1), + deltaLabelB: ngettext('More than %d hour ago', 'More than %d hours ago', 8).format(8), + deltaLabelC: ngettext('%d minute ago', '%d minutes ago', 18).format(18) + }), element.find('.notification-items')); + }); +}); + +QUnit.test('creme.NotificationBox (fetch)', function() { + var element = $(this.createNotificationBoxHtml()).appendTo(this.qunitFixture()); + var box = new creme.notification.NotificationBox(element, { + refreshDelay: 150, + refreshUrl: 'mock/notifs/refresh', + discardUrl: 'mock/notifs/discard' + }); + + stop(2); + + setTimeout(function() { + box.stopFetch(); + + var counter = element.find('.notification-box-count'); + + equal(counter.text(), '3'); + equal(counter.is('.is-empty'), false); + + deepEqual([], this.mockBackendUrlCalls('mock/notifs/discard')); + deepEqual([ + ['GET', {}], + ['GET', {}] + ], this.mockBackendUrlCalls('mock/notifs/refresh')); + + start(); + }.bind(this), 350); + + setTimeout(function() { + deepEqual([], this.mockBackendUrlCalls('mock/notifs/discard')); + + // no changes, the job is already stopped + deepEqual([ + ['GET', {}], + ['GET', {}] + ], this.mockBackendUrlCalls('mock/notifs/refresh')); + + start(); + }.bind(this), 350); +}); + +QUnit.test('creme.NotificationBox (fetch, update deltas)', function() { + var element = $(this.createNotificationBoxHtml({ + initialData: this.defaultNotificationData() + })).appendTo(this.qunitFixture()); + var box = new creme.notification.NotificationBox(element, { + deltaRefreshDelay: 150, + refreshUrl: 'mock/notifs/refresh', + discardUrl: 'mock/notifs/discard' + }).stopFetch(); + + var faker = new FunctionFaker(); + box._updateDeltas = faker.wrap(); + + equal(0, faker.calls()); + box.startFetch(); + + equal(0, faker.calls().length); + + stop(2); + + setTimeout(function() { + equal(2, faker.calls().length); + box.stopFetch(); + start(); + }, 350); + + setTimeout(function() { + // no changes, the job is already stopped + equal(2, faker.calls().length); + start(); + }, 450); +}); + +QUnit.test('creme.NotificationBox (fetch, error)', function() { + var element = $(this.createNotificationBoxHtml({ + initialData: this.defaultNotificationData() + })).appendTo(this.qunitFixture()); + + var box = new creme.notification.NotificationBox(element, { + refreshDelay: 150, + refreshUrl: 'mock/notifs/refresh/fail', + discardUrl: 'mock/notifs/discard' + }); + + stop(1); + + setTimeout(function() { + var counter = element.find('.notification-box-count'); + + equal(counter.text(), '3'); + equal(counter.is('.is-empty'), false); + + var errors = element.find('.notification-error'); + equal(errors.is('.is-empty'), false); + equal(errors.text(), gettext('An error happened when retrieving notifications (%s)').format('')); + + deepEqual([ + ['GET', {}] + ], this.mockBackendUrlCalls('mock/notifs/refresh/fail')); + + box.stopFetch(); + start(); + }.bind(this), 200); +}); + +QUnit.test('creme.NotificationBox (discard)', function() { + var element = $(this.createNotificationBoxHtml({ + initialData: this.defaultNotificationData() + })).appendTo(this.qunitFixture()); + + var box = new creme.notification.NotificationBox(element, { + refreshUrl: 'mock/notifs/refresh', + discardUrl: 'mock/notifs/discard' + }).stopFetch(); + + var counter = element.find('.notification-box-count'); + + equal(counter.text(), '3'); + equal(counter.is('.is-empty'), false); + equal(element.find('.notification-item .discard-notification').length, 3); + deepEqual([], this.mockBackendUrlCalls('mock/notifs/discard')); + + element.find('[data-id="2"] .discard-notification').trigger('click'); + + deepEqual([ + ['POST', {id: 2}] + ], this.mockBackendUrlCalls('mock/notifs/discard')); + + equal(element.find('.notification-item .discard-notification').length, 2); + + counter = element.find('.notification-box-count'); + equal(counter.text(), '2'); + equal(counter.is('.is-empty'), false); + + element.find('.discard-notification').trigger('click'); + + deepEqual([ + ['POST', {id: 2}], + ['POST', {id: 1}], + ['POST', {id: 3}] + ], this.mockBackendUrlCalls('mock/notifs/discard')); + + equal(element.find('.notification-item .discard-notification').length, 0); + + counter = element.find('.notification-box-count'); + equal(counter.text(), '0'); + equal(counter.is('.is-empty'), true); + + box.stopFetch(); +}); + +QUnit.test('creme.NotificationBox (fetch, document.hidden)', function() { + var element = $(this.createNotificationBoxHtml({ + initialData: this.defaultNotificationData() + })).appendTo(this.qunitFixture()); + + var hiddenFaker = new PropertyFaker({ + instance: document, props: {hidden: true} + }); + + var box = new creme.notification.NotificationBox(element, { + refreshDelay: 150, + refreshUrl: 'mock/notifs/refresh', + discardUrl: 'mock/notifs/discard' + }); + + hiddenFaker.with(function() { + equal(document.hidden, true); + equal(box.isFetchActive(), true); + equal(box.isPaused(), true); + + // when doc is hidden, the fetch is automatically stopped even without + // any event + box._fetchItems(); + + deepEqual([], this.mockBackendUrlCalls('mock/notifs/refresh')); + }.bind(this)); + + equal(document.hidden, false); + box.stopFetch(); +}); + +}(jQuery)); diff --git a/creme/creme_core/static/icecream/creme_core/css/header_menu.css b/creme/creme_core/static/icecream/creme_core/css/header_menu.css index e9d4a55dbe..27b4a5f46a 100644 --- a/creme/creme_core/static/icecream/creme_core/css/header_menu.css +++ b/creme/creme_core/static/icecream/creme_core/css/header_menu.css @@ -415,7 +415,7 @@ span.create-group-entry { box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5); } -.notification-box.notification-box-activated .notification-panel { +.notification-box:hover .notification-panel { visibility: visible; opacity: 1; } diff --git a/creme/creme_core/templates/creme_core/header/menu-base.html b/creme/creme_core/templates/creme_core/header/menu-base.html index ac432aac78..ca39caff22 100644 --- a/creme/creme_core/templates/creme_core/header/menu-base.html +++ b/creme/creme_core/templates/creme_core/header/menu-base.html @@ -65,18 +65,18 @@
      diff --git a/creme/settings.py b/creme/settings.py index 3d61115055..06ee4e5d56 100644 --- a/creme/settings.py +++ b/creme/settings.py @@ -1136,6 +1136,7 @@ 'creme_core/js/tests/views/menu.js', 'creme_core/js/tests/views/search.js', 'creme_core/js/tests/views/utils.js', + 'creme_core/js/tests/views/notification.js', ] TEST_CREME_OPT_JS = [ From b90fbfe9b41b1b861e8c928af8c42cffcd06516d Mon Sep 17 00:00:00 2001 From: joehybird Date: Tue, 21 Jan 2025 17:53:29 +0100 Subject: [PATCH 15/29] Improve bricks tests coverage --- .../static/creme_core/js/tests/brick/brick.js | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/creme/creme_core/static/creme_core/js/tests/brick/brick.js b/creme/creme_core/static/creme_core/js/tests/brick/brick.js index 8f1aa51454..c8a49efa44 100644 --- a/creme/creme_core/static/creme_core/js/tests/brick/brick.js +++ b/creme/creme_core/static/creme_core/js/tests/brick/brick.js @@ -767,6 +767,28 @@ QUnit.test('creme.bricks.Brick.refresh', function(assert) { ], this.mockBackendUrlCalls('mock/brick/all/reload')); }); + +QUnit.test('creme.bricks.Brick.refresh (from pager)', function(assert) { + var element = $( +// '
      ' + '
      ' + ).appendTo(this.qunitFixture()); + var widget = creme.widget.create(element); + var brick = widget.brick(); + + equal(true, brick.isBound()); +// equal('brick-for-test', brick.id()); + equal('brick-creme_core-test', brick.id()); + equal('creme_core-test', brick.type_id()); + + brick._pager.refresh(2); + deepEqual([ +// ['GET', {"brick_id": ["brick-for-test"], "extra_data": "{}"}] + ['GET', {"brick_id": ["creme_core-test"], "creme_core-test_page": 2, "extra_data": "{}"}] + ], this.mockBackendUrlCalls('mock/brick/all/reload')); +}); + + QUnit.test('creme.bricks.Brick.refresh (no deps)', function(assert) { // var htmlA = '
      '; // var htmlB = '
      '; @@ -947,4 +969,6 @@ QUnit.test('creme.bricks.Brick.refresh (wildcard deps)', function(assert) { ], this.mockBackendUrlCalls('mock/brick/all/reload')); }); + + }(jQuery)); From 95f38c41a2d532d800c9e514ccc4e68b133e8867 Mon Sep 17 00:00:00 2001 From: joehybird Date: Wed, 22 Jan 2025 14:10:46 +0100 Subject: [PATCH 16/29] Upgrade underscorejs to 1.13.7 --- CHANGELOG.txt | 1 + .../js/lib/underscore/underscore-1.13.7.js | 2047 +++++++++++++++++ creme/settings.py | 2 +- 3 files changed, 2049 insertions(+), 1 deletion(-) create mode 100644 creme/creme_core/static/creme_core/js/lib/underscore/underscore-1.13.7.js diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 762baa55a3..967d4c96e3 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -106,6 +106,7 @@ The class 'django.forms.fields.CallableChoiceIterator' has been removed ; use 'django.utils.choices.CallableChoiceIterator' instead. - https://docs.djangoproject.com/en/5.1/releases/5.1/ + - The version of 'underscorejs' is now '1.13.7'. Non breaking changes : ---------------------- diff --git a/creme/creme_core/static/creme_core/js/lib/underscore/underscore-1.13.7.js b/creme/creme_core/static/creme_core/js/lib/underscore/underscore-1.13.7.js new file mode 100644 index 0000000000..c7149914d9 --- /dev/null +++ b/creme/creme_core/static/creme_core/js/lib/underscore/underscore-1.13.7.js @@ -0,0 +1,2047 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define('underscore', factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, (function () { + var current = global._; + var exports = global._ = factory(); + exports.noConflict = function () { global._ = current; return exports; }; + }())); +}(this, (function () { + // Underscore.js 1.13.7 + // https://underscorejs.org + // (c) 2009-2024 Jeremy Ashkenas, Julian Gonggrijp, and DocumentCloud and Investigative Reporters & Editors + // Underscore may be freely distributed under the MIT license. + + // Current version. + var VERSION = '1.13.7'; + + // Establish the root object, `window` (`self`) in the browser, `global` + // on the server, or `this` in some virtual machines. We use `self` + // instead of `window` for `WebWorker` support. + var root = (typeof self == 'object' && self.self === self && self) || + (typeof global == 'object' && global.global === global && global) || + Function('return this')() || + {}; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype; + var SymbolProto = typeof Symbol !== 'undefined' ? Symbol.prototype : null; + + // Create quick reference variables for speed access to core prototypes. + var push = ArrayProto.push, + slice = ArrayProto.slice, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // Modern feature detection. + var supportsArrayBuffer = typeof ArrayBuffer !== 'undefined', + supportsDataView = typeof DataView !== 'undefined'; + + // All **ECMAScript 5+** native function implementations that we hope to use + // are declared here. + var nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeCreate = Object.create, + nativeIsView = supportsArrayBuffer && ArrayBuffer.isView; + + // Create references to these builtin functions because we override them. + var _isNaN = isNaN, + _isFinite = isFinite; + + // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed. + var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString'); + var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString', + 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString']; + + // The largest integer that can be represented exactly. + var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; + + // Some functions take a variable number of arguments, or a few expected + // arguments at the beginning and then a variable number of values to operate + // on. This helper accumulates all remaining arguments past the function’s + // argument length (or an explicit `startIndex`), into an array that becomes + // the last argument. Similar to ES6’s "rest parameter". + function restArguments(func, startIndex) { + startIndex = startIndex == null ? func.length - 1 : +startIndex; + return function() { + var length = Math.max(arguments.length - startIndex, 0), + rest = Array(length), + index = 0; + for (; index < length; index++) { + rest[index] = arguments[index + startIndex]; + } + switch (startIndex) { + case 0: return func.call(this, rest); + case 1: return func.call(this, arguments[0], rest); + case 2: return func.call(this, arguments[0], arguments[1], rest); + } + var args = Array(startIndex + 1); + for (index = 0; index < startIndex; index++) { + args[index] = arguments[index]; + } + args[startIndex] = rest; + return func.apply(this, args); + }; + } + + // Is a given variable an object? + function isObject(obj) { + var type = typeof obj; + return type === 'function' || (type === 'object' && !!obj); + } + + // Is a given value equal to null? + function isNull(obj) { + return obj === null; + } + + // Is a given variable undefined? + function isUndefined(obj) { + return obj === void 0; + } + + // Is a given value a boolean? + function isBoolean(obj) { + return obj === true || obj === false || toString.call(obj) === '[object Boolean]'; + } + + // Is a given value a DOM element? + function isElement(obj) { + return !!(obj && obj.nodeType === 1); + } + + // Internal function for creating a `toString`-based type tester. + function tagTester(name) { + var tag = '[object ' + name + ']'; + return function(obj) { + return toString.call(obj) === tag; + }; + } + + var isString = tagTester('String'); + + var isNumber = tagTester('Number'); + + var isDate = tagTester('Date'); + + var isRegExp = tagTester('RegExp'); + + var isError = tagTester('Error'); + + var isSymbol = tagTester('Symbol'); + + var isArrayBuffer = tagTester('ArrayBuffer'); + + var isFunction = tagTester('Function'); + + // Optimize `isFunction` if appropriate. Work around some `typeof` bugs in old + // v8, IE 11 (#1621), Safari 8 (#1929), and PhantomJS (#2236). + var nodelist = root.document && root.document.childNodes; + if (typeof /./ != 'function' && typeof Int8Array != 'object' && typeof nodelist != 'function') { + isFunction = function(obj) { + return typeof obj == 'function' || false; + }; + } + + var isFunction$1 = isFunction; + + var hasObjectTag = tagTester('Object'); + + // In IE 10 - Edge 13, `DataView` has string tag `'[object Object]'`. + // In IE 11, the most common among them, this problem also applies to + // `Map`, `WeakMap` and `Set`. + // Also, there are cases where an application can override the native + // `DataView` object, in cases like that we can't use the constructor + // safely and should just rely on alternate `DataView` checks + var hasDataViewBug = ( + supportsDataView && (!/\[native code\]/.test(String(DataView)) || hasObjectTag(new DataView(new ArrayBuffer(8)))) + ), + isIE11 = (typeof Map !== 'undefined' && hasObjectTag(new Map)); + + var isDataView = tagTester('DataView'); + + // In IE 10 - Edge 13, we need a different heuristic + // to determine whether an object is a `DataView`. + // Also, in cases where the native `DataView` is + // overridden we can't rely on the tag itself. + function alternateIsDataView(obj) { + return obj != null && isFunction$1(obj.getInt8) && isArrayBuffer(obj.buffer); + } + + var isDataView$1 = (hasDataViewBug ? alternateIsDataView : isDataView); + + // Is a given value an array? + // Delegates to ECMA5's native `Array.isArray`. + var isArray = nativeIsArray || tagTester('Array'); + + // Internal function to check whether `key` is an own property name of `obj`. + function has$1(obj, key) { + return obj != null && hasOwnProperty.call(obj, key); + } + + var isArguments = tagTester('Arguments'); + + // Define a fallback version of the method in browsers (ahem, IE < 9), where + // there isn't any inspectable "Arguments" type. + (function() { + if (!isArguments(arguments)) { + isArguments = function(obj) { + return has$1(obj, 'callee'); + }; + } + }()); + + var isArguments$1 = isArguments; + + // Is a given object a finite number? + function isFinite$1(obj) { + return !isSymbol(obj) && _isFinite(obj) && !isNaN(parseFloat(obj)); + } + + // Is the given value `NaN`? + function isNaN$1(obj) { + return isNumber(obj) && _isNaN(obj); + } + + // Predicate-generating function. Often useful outside of Underscore. + function constant(value) { + return function() { + return value; + }; + } + + // Common internal logic for `isArrayLike` and `isBufferLike`. + function createSizePropertyCheck(getSizeProperty) { + return function(collection) { + var sizeProperty = getSizeProperty(collection); + return typeof sizeProperty == 'number' && sizeProperty >= 0 && sizeProperty <= MAX_ARRAY_INDEX; + } + } + + // Internal helper to generate a function to obtain property `key` from `obj`. + function shallowProperty(key) { + return function(obj) { + return obj == null ? void 0 : obj[key]; + }; + } + + // Internal helper to obtain the `byteLength` property of an object. + var getByteLength = shallowProperty('byteLength'); + + // Internal helper to determine whether we should spend extensive checks against + // `ArrayBuffer` et al. + var isBufferLike = createSizePropertyCheck(getByteLength); + + // Is a given value a typed array? + var typedArrayPattern = /\[object ((I|Ui)nt(8|16|32)|Float(32|64)|Uint8Clamped|Big(I|Ui)nt64)Array\]/; + function isTypedArray(obj) { + // `ArrayBuffer.isView` is the most future-proof, so use it when available. + // Otherwise, fall back on the above regular expression. + return nativeIsView ? (nativeIsView(obj) && !isDataView$1(obj)) : + isBufferLike(obj) && typedArrayPattern.test(toString.call(obj)); + } + + var isTypedArray$1 = supportsArrayBuffer ? isTypedArray : constant(false); + + // Internal helper to obtain the `length` property of an object. + var getLength = shallowProperty('length'); + + // Internal helper to create a simple lookup structure. + // `collectNonEnumProps` used to depend on `_.contains`, but this led to + // circular imports. `emulatedSet` is a one-off solution that only works for + // arrays of strings. + function emulatedSet(keys) { + var hash = {}; + for (var l = keys.length, i = 0; i < l; ++i) hash[keys[i]] = true; + return { + contains: function(key) { return hash[key] === true; }, + push: function(key) { + hash[key] = true; + return keys.push(key); + } + }; + } + + // Internal helper. Checks `keys` for the presence of keys in IE < 9 that won't + // be iterated by `for key in ...` and thus missed. Extends `keys` in place if + // needed. + function collectNonEnumProps(obj, keys) { + keys = emulatedSet(keys); + var nonEnumIdx = nonEnumerableProps.length; + var constructor = obj.constructor; + var proto = (isFunction$1(constructor) && constructor.prototype) || ObjProto; + + // Constructor is a special case. + var prop = 'constructor'; + if (has$1(obj, prop) && !keys.contains(prop)) keys.push(prop); + + while (nonEnumIdx--) { + prop = nonEnumerableProps[nonEnumIdx]; + if (prop in obj && obj[prop] !== proto[prop] && !keys.contains(prop)) { + keys.push(prop); + } + } + } + + // Retrieve the names of an object's own properties. + // Delegates to **ECMAScript 5**'s native `Object.keys`. + function keys(obj) { + if (!isObject(obj)) return []; + if (nativeKeys) return nativeKeys(obj); + var keys = []; + for (var key in obj) if (has$1(obj, key)) keys.push(key); + // Ahem, IE < 9. + if (hasEnumBug) collectNonEnumProps(obj, keys); + return keys; + } + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + function isEmpty(obj) { + if (obj == null) return true; + // Skip the more expensive `toString`-based type checks if `obj` has no + // `.length`. + var length = getLength(obj); + if (typeof length == 'number' && ( + isArray(obj) || isString(obj) || isArguments$1(obj) + )) return length === 0; + return getLength(keys(obj)) === 0; + } + + // Returns whether an object has a given set of `key:value` pairs. + function isMatch(object, attrs) { + var _keys = keys(attrs), length = _keys.length; + if (object == null) return !length; + var obj = Object(object); + for (var i = 0; i < length; i++) { + var key = _keys[i]; + if (attrs[key] !== obj[key] || !(key in obj)) return false; + } + return true; + } + + // If Underscore is called as a function, it returns a wrapped object that can + // be used OO-style. This wrapper holds altered versions of all functions added + // through `_.mixin`. Wrapped objects may be chained. + function _$1(obj) { + if (obj instanceof _$1) return obj; + if (!(this instanceof _$1)) return new _$1(obj); + this._wrapped = obj; + } + + _$1.VERSION = VERSION; + + // Extracts the result from a wrapped and chained object. + _$1.prototype.value = function() { + return this._wrapped; + }; + + // Provide unwrapping proxies for some methods used in engine operations + // such as arithmetic and JSON stringification. + _$1.prototype.valueOf = _$1.prototype.toJSON = _$1.prototype.value; + + _$1.prototype.toString = function() { + return String(this._wrapped); + }; + + // Internal function to wrap or shallow-copy an ArrayBuffer, + // typed array or DataView to a new view, reusing the buffer. + function toBufferView(bufferSource) { + return new Uint8Array( + bufferSource.buffer || bufferSource, + bufferSource.byteOffset || 0, + getByteLength(bufferSource) + ); + } + + // We use this string twice, so give it a name for minification. + var tagDataView = '[object DataView]'; + + // Internal recursive comparison function for `_.isEqual`. + function eq(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) return a !== 0 || 1 / a === 1 / b; + // `null` or `undefined` only equal to itself (strict comparison). + if (a == null || b == null) return false; + // `NaN`s are equivalent, but non-reflexive. + if (a !== a) return b !== b; + // Exhaust primitive checks + var type = typeof a; + if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; + return deepEq(a, b, aStack, bStack); + } + + // Internal recursive comparison function for `_.isEqual`. + function deepEq(a, b, aStack, bStack) { + // Unwrap any wrapped objects. + if (a instanceof _$1) a = a._wrapped; + if (b instanceof _$1) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className !== toString.call(b)) return false; + // Work around a bug in IE 10 - Edge 13. + if (hasDataViewBug && className == '[object Object]' && isDataView$1(a)) { + if (!isDataView$1(b)) return false; + className = tagDataView; + } + switch (className) { + // These types are compared by value. + case '[object RegExp]': + // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return '' + a === '' + b; + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. + // Object(NaN) is equivalent to NaN. + if (+a !== +a) return +b !== +b; + // An `egal` comparison is performed for other numeric values. + return +a === 0 ? 1 / +a === 1 / b : +a === +b; + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a === +b; + case '[object Symbol]': + return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b); + case '[object ArrayBuffer]': + case tagDataView: + // Coerce to typed array so we can fall through. + return deepEq(toBufferView(a), toBufferView(b), aStack, bStack); + } + + var areArrays = className === '[object Array]'; + if (!areArrays && isTypedArray$1(a)) { + var byteLength = getByteLength(a); + if (byteLength !== getByteLength(b)) return false; + if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) return true; + areArrays = true; + } + if (!areArrays) { + if (typeof a != 'object' || typeof b != 'object') return false; + + // Objects with different constructors are not equivalent, but `Object`s or `Array`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(isFunction$1(aCtor) && aCtor instanceof aCtor && + isFunction$1(bCtor) && bCtor instanceof bCtor) + && ('constructor' in a && 'constructor' in b)) { + return false; + } + } + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + + // Initializing stack of traversed objects. + // It's done here since we only need them for objects and arrays comparison. + aStack = aStack || []; + bStack = bStack || []; + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] === a) return bStack[length] === b; + } + + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + + // Recursively compare objects and arrays. + if (areArrays) { + // Compare array lengths to determine if a deep comparison is necessary. + length = a.length; + if (length !== b.length) return false; + // Deep compare the contents, ignoring non-numeric properties. + while (length--) { + if (!eq(a[length], b[length], aStack, bStack)) return false; + } + } else { + // Deep compare objects. + var _keys = keys(a), key; + length = _keys.length; + // Ensure that both objects contain the same number of properties before comparing deep equality. + if (keys(b).length !== length) return false; + while (length--) { + // Deep compare each member + key = _keys[length]; + if (!(has$1(b, key) && eq(a[key], b[key], aStack, bStack))) return false; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return true; + } + + // Perform a deep comparison to check if two objects are equal. + function isEqual(a, b) { + return eq(a, b); + } + + // Retrieve all the enumerable property names of an object. + function allKeys(obj) { + if (!isObject(obj)) return []; + var keys = []; + for (var key in obj) keys.push(key); + // Ahem, IE < 9. + if (hasEnumBug) collectNonEnumProps(obj, keys); + return keys; + } + + // Since the regular `Object.prototype.toString` type tests don't work for + // some types in IE 11, we use a fingerprinting heuristic instead, based + // on the methods. It's not great, but it's the best we got. + // The fingerprint method lists are defined below. + function ie11fingerprint(methods) { + var length = getLength(methods); + return function(obj) { + if (obj == null) return false; + // `Map`, `WeakMap` and `Set` have no enumerable keys. + var keys = allKeys(obj); + if (getLength(keys)) return false; + for (var i = 0; i < length; i++) { + if (!isFunction$1(obj[methods[i]])) return false; + } + // If we are testing against `WeakMap`, we need to ensure that + // `obj` doesn't have a `forEach` method in order to distinguish + // it from a regular `Map`. + return methods !== weakMapMethods || !isFunction$1(obj[forEachName]); + }; + } + + // In the interest of compact minification, we write + // each string in the fingerprints only once. + var forEachName = 'forEach', + hasName = 'has', + commonInit = ['clear', 'delete'], + mapTail = ['get', hasName, 'set']; + + // `Map`, `WeakMap` and `Set` each have slightly different + // combinations of the above sublists. + var mapMethods = commonInit.concat(forEachName, mapTail), + weakMapMethods = commonInit.concat(mapTail), + setMethods = ['add'].concat(commonInit, forEachName, hasName); + + var isMap = isIE11 ? ie11fingerprint(mapMethods) : tagTester('Map'); + + var isWeakMap = isIE11 ? ie11fingerprint(weakMapMethods) : tagTester('WeakMap'); + + var isSet = isIE11 ? ie11fingerprint(setMethods) : tagTester('Set'); + + var isWeakSet = tagTester('WeakSet'); + + // Retrieve the values of an object's properties. + function values(obj) { + var _keys = keys(obj); + var length = _keys.length; + var values = Array(length); + for (var i = 0; i < length; i++) { + values[i] = obj[_keys[i]]; + } + return values; + } + + // Convert an object into a list of `[key, value]` pairs. + // The opposite of `_.object` with one argument. + function pairs(obj) { + var _keys = keys(obj); + var length = _keys.length; + var pairs = Array(length); + for (var i = 0; i < length; i++) { + pairs[i] = [_keys[i], obj[_keys[i]]]; + } + return pairs; + } + + // Invert the keys and values of an object. The values must be serializable. + function invert(obj) { + var result = {}; + var _keys = keys(obj); + for (var i = 0, length = _keys.length; i < length; i++) { + result[obj[_keys[i]]] = _keys[i]; + } + return result; + } + + // Return a sorted list of the function names available on the object. + function functions(obj) { + var names = []; + for (var key in obj) { + if (isFunction$1(obj[key])) names.push(key); + } + return names.sort(); + } + + // An internal function for creating assigner functions. + function createAssigner(keysFunc, defaults) { + return function(obj) { + var length = arguments.length; + if (defaults) obj = Object(obj); + if (length < 2 || obj == null) return obj; + for (var index = 1; index < length; index++) { + var source = arguments[index], + keys = keysFunc(source), + l = keys.length; + for (var i = 0; i < l; i++) { + var key = keys[i]; + if (!defaults || obj[key] === void 0) obj[key] = source[key]; + } + } + return obj; + }; + } + + // Extend a given object with all the properties in passed-in object(s). + var extend = createAssigner(allKeys); + + // Assigns a given object with all the own properties in the passed-in + // object(s). + // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) + var extendOwn = createAssigner(keys); + + // Fill in a given object with default properties. + var defaults = createAssigner(allKeys, true); + + // Create a naked function reference for surrogate-prototype-swapping. + function ctor() { + return function(){}; + } + + // An internal function for creating a new object that inherits from another. + function baseCreate(prototype) { + if (!isObject(prototype)) return {}; + if (nativeCreate) return nativeCreate(prototype); + var Ctor = ctor(); + Ctor.prototype = prototype; + var result = new Ctor; + Ctor.prototype = null; + return result; + } + + // Creates an object that inherits from the given prototype object. + // If additional properties are provided then they will be added to the + // created object. + function create(prototype, props) { + var result = baseCreate(prototype); + if (props) extendOwn(result, props); + return result; + } + + // Create a (shallow-cloned) duplicate of an object. + function clone(obj) { + if (!isObject(obj)) return obj; + return isArray(obj) ? obj.slice() : extend({}, obj); + } + + // Invokes `interceptor` with the `obj` and then returns `obj`. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + function tap(obj, interceptor) { + interceptor(obj); + return obj; + } + + // Normalize a (deep) property `path` to array. + // Like `_.iteratee`, this function can be customized. + function toPath$1(path) { + return isArray(path) ? path : [path]; + } + _$1.toPath = toPath$1; + + // Internal wrapper for `_.toPath` to enable minification. + // Similar to `cb` for `_.iteratee`. + function toPath(path) { + return _$1.toPath(path); + } + + // Internal function to obtain a nested property in `obj` along `path`. + function deepGet(obj, path) { + var length = path.length; + for (var i = 0; i < length; i++) { + if (obj == null) return void 0; + obj = obj[path[i]]; + } + return length ? obj : void 0; + } + + // Get the value of the (deep) property on `path` from `object`. + // If any property in `path` does not exist or if the value is + // `undefined`, return `defaultValue` instead. + // The `path` is normalized through `_.toPath`. + function get(object, path, defaultValue) { + var value = deepGet(object, toPath(path)); + return isUndefined(value) ? defaultValue : value; + } + + // Shortcut function for checking if an object has a given property directly on + // itself (in other words, not on a prototype). Unlike the internal `has` + // function, this public version can also traverse nested properties. + function has(obj, path) { + path = toPath(path); + var length = path.length; + for (var i = 0; i < length; i++) { + var key = path[i]; + if (!has$1(obj, key)) return false; + obj = obj[key]; + } + return !!length; + } + + // Keep the identity function around for default iteratees. + function identity(value) { + return value; + } + + // Returns a predicate for checking whether an object has a given set of + // `key:value` pairs. + function matcher(attrs) { + attrs = extendOwn({}, attrs); + return function(obj) { + return isMatch(obj, attrs); + }; + } + + // Creates a function that, when passed an object, will traverse that object’s + // properties down the given `path`, specified as an array of keys or indices. + function property(path) { + path = toPath(path); + return function(obj) { + return deepGet(obj, path); + }; + } + + // Internal function that returns an efficient (for current engines) version + // of the passed-in callback, to be repeatedly applied in other Underscore + // functions. + function optimizeCb(func, context, argCount) { + if (context === void 0) return func; + switch (argCount == null ? 3 : argCount) { + case 1: return function(value) { + return func.call(context, value); + }; + // The 2-argument case is omitted because we’re not using it. + case 3: return function(value, index, collection) { + return func.call(context, value, index, collection); + }; + case 4: return function(accumulator, value, index, collection) { + return func.call(context, accumulator, value, index, collection); + }; + } + return function() { + return func.apply(context, arguments); + }; + } + + // An internal function to generate callbacks that can be applied to each + // element in a collection, returning the desired result — either `_.identity`, + // an arbitrary callback, a property matcher, or a property accessor. + function baseIteratee(value, context, argCount) { + if (value == null) return identity; + if (isFunction$1(value)) return optimizeCb(value, context, argCount); + if (isObject(value) && !isArray(value)) return matcher(value); + return property(value); + } + + // External wrapper for our callback generator. Users may customize + // `_.iteratee` if they want additional predicate/iteratee shorthand styles. + // This abstraction hides the internal-only `argCount` argument. + function iteratee(value, context) { + return baseIteratee(value, context, Infinity); + } + _$1.iteratee = iteratee; + + // The function we call internally to generate a callback. It invokes + // `_.iteratee` if overridden, otherwise `baseIteratee`. + function cb(value, context, argCount) { + if (_$1.iteratee !== iteratee) return _$1.iteratee(value, context); + return baseIteratee(value, context, argCount); + } + + // Returns the results of applying the `iteratee` to each element of `obj`. + // In contrast to `_.map` it returns an object. + function mapObject(obj, iteratee, context) { + iteratee = cb(iteratee, context); + var _keys = keys(obj), + length = _keys.length, + results = {}; + for (var index = 0; index < length; index++) { + var currentKey = _keys[index]; + results[currentKey] = iteratee(obj[currentKey], currentKey, obj); + } + return results; + } + + // Predicate-generating function. Often useful outside of Underscore. + function noop(){} + + // Generates a function for a given object that returns a given property. + function propertyOf(obj) { + if (obj == null) return noop; + return function(path) { + return get(obj, path); + }; + } + + // Run a function **n** times. + function times(n, iteratee, context) { + var accum = Array(Math.max(0, n)); + iteratee = optimizeCb(iteratee, context, 1); + for (var i = 0; i < n; i++) accum[i] = iteratee(i); + return accum; + } + + // Return a random integer between `min` and `max` (inclusive). + function random(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + } + + // A (possibly faster) way to get the current timestamp as an integer. + var now = Date.now || function() { + return new Date().getTime(); + }; + + // Internal helper to generate functions for escaping and unescaping strings + // to/from HTML interpolation. + function createEscaper(map) { + var escaper = function(match) { + return map[match]; + }; + // Regexes for identifying a key that needs to be escaped. + var source = '(?:' + keys(map).join('|') + ')'; + var testRegexp = RegExp(source); + var replaceRegexp = RegExp(source, 'g'); + return function(string) { + string = string == null ? '' : '' + string; + return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; + }; + } + + // Internal list of HTML entities for escaping. + var escapeMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`' + }; + + // Function for escaping strings to HTML interpolation. + var _escape = createEscaper(escapeMap); + + // Internal list of HTML entities for unescaping. + var unescapeMap = invert(escapeMap); + + // Function for unescaping strings from HTML interpolation. + var _unescape = createEscaper(unescapeMap); + + // By default, Underscore uses ERB-style template delimiters. Change the + // following template settings to use alternative delimiters. + var templateSettings = _$1.templateSettings = { + evaluate: /<%([\s\S]+?)%>/g, + interpolate: /<%=([\s\S]+?)%>/g, + escape: /<%-([\s\S]+?)%>/g + }; + + // When customizing `_.templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g; + + function escapeChar(match) { + return '\\' + escapes[match]; + } + + // In order to prevent third-party code injection through + // `_.templateSettings.variable`, we test it against the following regular + // expression. It is intentionally a bit more liberal than just matching valid + // identifiers, but still prevents possible loopholes through defaults or + // destructuring assignment. + var bareIdentifier = /^\s*(\w|\$)+\s*$/; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + // NB: `oldSettings` only exists for backwards compatibility. + function template(text, settings, oldSettings) { + if (!settings && oldSettings) settings = oldSettings; + settings = defaults({}, settings, _$1.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset).replace(escapeRegExp, escapeChar); + index = offset + match.length; + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } else if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } else if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + + // Adobe VMs need the match returned to produce the correct offset. + return match; + }); + source += "';\n"; + + var argument = settings.variable; + if (argument) { + // Insure against third-party code injection. (CVE-2021-23358) + if (!bareIdentifier.test(argument)) throw new Error( + 'variable is not a bare identifier: ' + argument + ); + } else { + // If a variable is not specified, place data values in local scope. + source = 'with(obj||{}){\n' + source + '}\n'; + argument = 'obj'; + } + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + 'return __p;\n'; + + var render; + try { + render = new Function(argument, '_', source); + } catch (e) { + e.source = source; + throw e; + } + + var template = function(data) { + return render.call(this, data, _$1); + }; + + // Provide the compiled source as a convenience for precompilation. + template.source = 'function(' + argument + '){\n' + source + '}'; + + return template; + } + + // Traverses the children of `obj` along `path`. If a child is a function, it + // is invoked with its parent as context. Returns the value of the final + // child, or `fallback` if any child is undefined. + function result(obj, path, fallback) { + path = toPath(path); + var length = path.length; + if (!length) { + return isFunction$1(fallback) ? fallback.call(obj) : fallback; + } + for (var i = 0; i < length; i++) { + var prop = obj == null ? void 0 : obj[path[i]]; + if (prop === void 0) { + prop = fallback; + i = length; // Ensure we don't continue iterating. + } + obj = isFunction$1(prop) ? prop.call(obj) : prop; + } + return obj; + } + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + function uniqueId(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + } + + // Start chaining a wrapped Underscore object. + function chain(obj) { + var instance = _$1(obj); + instance._chain = true; + return instance; + } + + // Internal function to execute `sourceFunc` bound to `context` with optional + // `args`. Determines whether to execute a function as a constructor or as a + // normal function. + function executeBound(sourceFunc, boundFunc, context, callingContext, args) { + if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); + var self = baseCreate(sourceFunc.prototype); + var result = sourceFunc.apply(self, args); + if (isObject(result)) return result; + return self; + } + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. `_` acts + // as a placeholder by default, allowing any combination of arguments to be + // pre-filled. Set `_.partial.placeholder` for a custom placeholder argument. + var partial = restArguments(function(func, boundArgs) { + var placeholder = partial.placeholder; + var bound = function() { + var position = 0, length = boundArgs.length; + var args = Array(length); + for (var i = 0; i < length; i++) { + args[i] = boundArgs[i] === placeholder ? arguments[position++] : boundArgs[i]; + } + while (position < arguments.length) args.push(arguments[position++]); + return executeBound(func, bound, this, this, args); + }; + return bound; + }); + + partial.placeholder = _$1; + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). + var bind = restArguments(function(func, context, args) { + if (!isFunction$1(func)) throw new TypeError('Bind must be called on a function'); + var bound = restArguments(function(callArgs) { + return executeBound(func, bound, context, this, args.concat(callArgs)); + }); + return bound; + }); + + // Internal helper for collection methods to determine whether a collection + // should be iterated as an array or as an object. + // Related: https://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength + // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094 + var isArrayLike = createSizePropertyCheck(getLength); + + // Internal implementation of a recursive `flatten` function. + function flatten$1(input, depth, strict, output) { + output = output || []; + if (!depth && depth !== 0) { + depth = Infinity; + } else if (depth <= 0) { + return output.concat(input); + } + var idx = output.length; + for (var i = 0, length = getLength(input); i < length; i++) { + var value = input[i]; + if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) { + // Flatten current level of array or arguments object. + if (depth > 1) { + flatten$1(value, depth - 1, strict, output); + idx = output.length; + } else { + var j = 0, len = value.length; + while (j < len) output[idx++] = value[j++]; + } + } else if (!strict) { + output[idx++] = value; + } + } + return output; + } + + // Bind a number of an object's methods to that object. Remaining arguments + // are the method names to be bound. Useful for ensuring that all callbacks + // defined on an object belong to it. + var bindAll = restArguments(function(obj, keys) { + keys = flatten$1(keys, false, false); + var index = keys.length; + if (index < 1) throw new Error('bindAll must be passed function names'); + while (index--) { + var key = keys[index]; + obj[key] = bind(obj[key], obj); + } + return obj; + }); + + // Memoize an expensive function by storing its results. + function memoize(func, hasher) { + var memoize = function(key) { + var cache = memoize.cache; + var address = '' + (hasher ? hasher.apply(this, arguments) : key); + if (!has$1(cache, address)) cache[address] = func.apply(this, arguments); + return cache[address]; + }; + memoize.cache = {}; + return memoize; + } + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + var delay = restArguments(function(func, wait, args) { + return setTimeout(function() { + return func.apply(null, args); + }, wait); + }); + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + var defer = partial(delay, _$1, 1); + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. Normally, the throttled function will run + // as much as it can, without ever going more than once per `wait` duration; + // but if you'd like to disable the execution on the leading edge, pass + // `{leading: false}`. To disable execution on the trailing edge, ditto. + function throttle(func, wait, options) { + var timeout, context, args, result; + var previous = 0; + if (!options) options = {}; + + var later = function() { + previous = options.leading === false ? 0 : now(); + timeout = null; + result = func.apply(context, args); + if (!timeout) context = args = null; + }; + + var throttled = function() { + var _now = now(); + if (!previous && options.leading === false) previous = _now; + var remaining = wait - (_now - previous); + context = this; + args = arguments; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = _now; + result = func.apply(context, args); + if (!timeout) context = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + + throttled.cancel = function() { + clearTimeout(timeout); + previous = 0; + timeout = context = args = null; + }; + + return throttled; + } + + // When a sequence of calls of the returned function ends, the argument + // function is triggered. The end of a sequence is defined by the `wait` + // parameter. If `immediate` is passed, the argument function will be + // triggered at the beginning of the sequence instead of at the end. + function debounce(func, wait, immediate) { + var timeout, previous, args, result, context; + + var later = function() { + var passed = now() - previous; + if (wait > passed) { + timeout = setTimeout(later, wait - passed); + } else { + timeout = null; + if (!immediate) result = func.apply(context, args); + // This check is needed because `func` can recursively invoke `debounced`. + if (!timeout) args = context = null; + } + }; + + var debounced = restArguments(function(_args) { + context = this; + args = _args; + previous = now(); + if (!timeout) { + timeout = setTimeout(later, wait); + if (immediate) result = func.apply(context, args); + } + return result; + }); + + debounced.cancel = function() { + clearTimeout(timeout); + timeout = args = context = null; + }; + + return debounced; + } + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + function wrap(func, wrapper) { + return partial(wrapper, func); + } + + // Returns a negated version of the passed-in predicate. + function negate(predicate) { + return function() { + return !predicate.apply(this, arguments); + }; + } + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + function compose() { + var args = arguments; + var start = args.length - 1; + return function() { + var i = start; + var result = args[start].apply(this, arguments); + while (i--) result = args[i].call(this, result); + return result; + }; + } + + // Returns a function that will only be executed on and after the Nth call. + function after(times, func) { + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + } + + // Returns a function that will only be executed up to (but not including) the + // Nth call. + function before(times, func) { + var memo; + return function() { + if (--times > 0) { + memo = func.apply(this, arguments); + } + if (times <= 1) func = null; + return memo; + }; + } + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + var once = partial(before, 2); + + // Returns the first key on an object that passes a truth test. + function findKey(obj, predicate, context) { + predicate = cb(predicate, context); + var _keys = keys(obj), key; + for (var i = 0, length = _keys.length; i < length; i++) { + key = _keys[i]; + if (predicate(obj[key], key, obj)) return key; + } + } + + // Internal function to generate `_.findIndex` and `_.findLastIndex`. + function createPredicateIndexFinder(dir) { + return function(array, predicate, context) { + predicate = cb(predicate, context); + var length = getLength(array); + var index = dir > 0 ? 0 : length - 1; + for (; index >= 0 && index < length; index += dir) { + if (predicate(array[index], index, array)) return index; + } + return -1; + }; + } + + // Returns the first index on an array-like that passes a truth test. + var findIndex = createPredicateIndexFinder(1); + + // Returns the last index on an array-like that passes a truth test. + var findLastIndex = createPredicateIndexFinder(-1); + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + function sortedIndex(array, obj, iteratee, context) { + iteratee = cb(iteratee, context, 1); + var value = iteratee(obj); + var low = 0, high = getLength(array); + while (low < high) { + var mid = Math.floor((low + high) / 2); + if (iteratee(array[mid]) < value) low = mid + 1; else high = mid; + } + return low; + } + + // Internal function to generate the `_.indexOf` and `_.lastIndexOf` functions. + function createIndexFinder(dir, predicateFind, sortedIndex) { + return function(array, item, idx) { + var i = 0, length = getLength(array); + if (typeof idx == 'number') { + if (dir > 0) { + i = idx >= 0 ? idx : Math.max(idx + length, i); + } else { + length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1; + } + } else if (sortedIndex && idx && length) { + idx = sortedIndex(array, item); + return array[idx] === item ? idx : -1; + } + if (item !== item) { + idx = predicateFind(slice.call(array, i, length), isNaN$1); + return idx >= 0 ? idx + i : -1; + } + for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) { + if (array[idx] === item) return idx; + } + return -1; + }; + } + + // Return the position of the first occurrence of an item in an array, + // or -1 if the item is not included in the array. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + var indexOf = createIndexFinder(1, findIndex, sortedIndex); + + // Return the position of the last occurrence of an item in an array, + // or -1 if the item is not included in the array. + var lastIndexOf = createIndexFinder(-1, findLastIndex); + + // Return the first value which passes a truth test. + function find(obj, predicate, context) { + var keyFinder = isArrayLike(obj) ? findIndex : findKey; + var key = keyFinder(obj, predicate, context); + if (key !== void 0 && key !== -1) return obj[key]; + } + + // Convenience version of a common use case of `_.find`: getting the first + // object containing specific `key:value` pairs. + function findWhere(obj, attrs) { + return find(obj, matcher(attrs)); + } + + // The cornerstone for collection functions, an `each` + // implementation, aka `forEach`. + // Handles raw objects in addition to array-likes. Treats all + // sparse array-likes as if they were dense. + function each(obj, iteratee, context) { + iteratee = optimizeCb(iteratee, context); + var i, length; + if (isArrayLike(obj)) { + for (i = 0, length = obj.length; i < length; i++) { + iteratee(obj[i], i, obj); + } + } else { + var _keys = keys(obj); + for (i = 0, length = _keys.length; i < length; i++) { + iteratee(obj[_keys[i]], _keys[i], obj); + } + } + return obj; + } + + // Return the results of applying the iteratee to each element. + function map(obj, iteratee, context) { + iteratee = cb(iteratee, context); + var _keys = !isArrayLike(obj) && keys(obj), + length = (_keys || obj).length, + results = Array(length); + for (var index = 0; index < length; index++) { + var currentKey = _keys ? _keys[index] : index; + results[index] = iteratee(obj[currentKey], currentKey, obj); + } + return results; + } + + // Internal helper to create a reducing function, iterating left or right. + function createReduce(dir) { + // Wrap code that reassigns argument variables in a separate function than + // the one that accesses `arguments.length` to avoid a perf hit. (#1991) + var reducer = function(obj, iteratee, memo, initial) { + var _keys = !isArrayLike(obj) && keys(obj), + length = (_keys || obj).length, + index = dir > 0 ? 0 : length - 1; + if (!initial) { + memo = obj[_keys ? _keys[index] : index]; + index += dir; + } + for (; index >= 0 && index < length; index += dir) { + var currentKey = _keys ? _keys[index] : index; + memo = iteratee(memo, obj[currentKey], currentKey, obj); + } + return memo; + }; + + return function(obj, iteratee, memo, context) { + var initial = arguments.length >= 3; + return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial); + }; + } + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. + var reduce = createReduce(1); + + // The right-associative version of reduce, also known as `foldr`. + var reduceRight = createReduce(-1); + + // Return all the elements that pass a truth test. + function filter(obj, predicate, context) { + var results = []; + predicate = cb(predicate, context); + each(obj, function(value, index, list) { + if (predicate(value, index, list)) results.push(value); + }); + return results; + } + + // Return all the elements for which a truth test fails. + function reject(obj, predicate, context) { + return filter(obj, negate(cb(predicate)), context); + } + + // Determine whether all of the elements pass a truth test. + function every(obj, predicate, context) { + predicate = cb(predicate, context); + var _keys = !isArrayLike(obj) && keys(obj), + length = (_keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = _keys ? _keys[index] : index; + if (!predicate(obj[currentKey], currentKey, obj)) return false; + } + return true; + } + + // Determine if at least one element in the object passes a truth test. + function some(obj, predicate, context) { + predicate = cb(predicate, context); + var _keys = !isArrayLike(obj) && keys(obj), + length = (_keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = _keys ? _keys[index] : index; + if (predicate(obj[currentKey], currentKey, obj)) return true; + } + return false; + } + + // Determine if the array or object contains a given item (using `===`). + function contains(obj, item, fromIndex, guard) { + if (!isArrayLike(obj)) obj = values(obj); + if (typeof fromIndex != 'number' || guard) fromIndex = 0; + return indexOf(obj, item, fromIndex) >= 0; + } + + // Invoke a method (with arguments) on every item in a collection. + var invoke = restArguments(function(obj, path, args) { + var contextPath, func; + if (isFunction$1(path)) { + func = path; + } else { + path = toPath(path); + contextPath = path.slice(0, -1); + path = path[path.length - 1]; + } + return map(obj, function(context) { + var method = func; + if (!method) { + if (contextPath && contextPath.length) { + context = deepGet(context, contextPath); + } + if (context == null) return void 0; + method = context[path]; + } + return method == null ? method : method.apply(context, args); + }); + }); + + // Convenience version of a common use case of `_.map`: fetching a property. + function pluck(obj, key) { + return map(obj, property(key)); + } + + // Convenience version of a common use case of `_.filter`: selecting only + // objects containing specific `key:value` pairs. + function where(obj, attrs) { + return filter(obj, matcher(attrs)); + } + + // Return the maximum element (or element-based computation). + function max(obj, iteratee, context) { + var result = -Infinity, lastComputed = -Infinity, + value, computed; + if (iteratee == null || (typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null)) { + obj = isArrayLike(obj) ? obj : values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value != null && value > result) { + result = value; + } + } + } else { + iteratee = cb(iteratee, context); + each(obj, function(v, index, list) { + computed = iteratee(v, index, list); + if (computed > lastComputed || (computed === -Infinity && result === -Infinity)) { + result = v; + lastComputed = computed; + } + }); + } + return result; + } + + // Return the minimum element (or element-based computation). + function min(obj, iteratee, context) { + var result = Infinity, lastComputed = Infinity, + value, computed; + if (iteratee == null || (typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null)) { + obj = isArrayLike(obj) ? obj : values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value != null && value < result) { + result = value; + } + } + } else { + iteratee = cb(iteratee, context); + each(obj, function(v, index, list) { + computed = iteratee(v, index, list); + if (computed < lastComputed || (computed === Infinity && result === Infinity)) { + result = v; + lastComputed = computed; + } + }); + } + return result; + } + + // Safely create a real, live array from anything iterable. + var reStrSymbol = /[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g; + function toArray(obj) { + if (!obj) return []; + if (isArray(obj)) return slice.call(obj); + if (isString(obj)) { + // Keep surrogate pair characters together. + return obj.match(reStrSymbol); + } + if (isArrayLike(obj)) return map(obj, identity); + return values(obj); + } + + // Sample **n** random values from a collection using the modern version of the + // [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher–Yates_shuffle). + // If **n** is not specified, returns a single random element. + // The internal `guard` argument allows it to work with `_.map`. + function sample(obj, n, guard) { + if (n == null || guard) { + if (!isArrayLike(obj)) obj = values(obj); + return obj[random(obj.length - 1)]; + } + var sample = toArray(obj); + var length = getLength(sample); + n = Math.max(Math.min(n, length), 0); + var last = length - 1; + for (var index = 0; index < n; index++) { + var rand = random(index, last); + var temp = sample[index]; + sample[index] = sample[rand]; + sample[rand] = temp; + } + return sample.slice(0, n); + } + + // Shuffle a collection. + function shuffle(obj) { + return sample(obj, Infinity); + } + + // Sort the object's values by a criterion produced by an iteratee. + function sortBy(obj, iteratee, context) { + var index = 0; + iteratee = cb(iteratee, context); + return pluck(map(obj, function(value, key, list) { + return { + value: value, + index: index++, + criteria: iteratee(value, key, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index - right.index; + }), 'value'); + } + + // An internal function used for aggregate "group by" operations. + function group(behavior, partition) { + return function(obj, iteratee, context) { + var result = partition ? [[], []] : {}; + iteratee = cb(iteratee, context); + each(obj, function(value, index) { + var key = iteratee(value, index, obj); + behavior(result, value, key); + }); + return result; + }; + } + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + var groupBy = group(function(result, value, key) { + if (has$1(result, key)) result[key].push(value); else result[key] = [value]; + }); + + // Indexes the object's values by a criterion, similar to `_.groupBy`, but for + // when you know that your index values will be unique. + var indexBy = group(function(result, value, key) { + result[key] = value; + }); + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + var countBy = group(function(result, value, key) { + if (has$1(result, key)) result[key]++; else result[key] = 1; + }); + + // Split a collection into two arrays: one whose elements all pass the given + // truth test, and one whose elements all do not pass the truth test. + var partition = group(function(result, value, pass) { + result[pass ? 0 : 1].push(value); + }, true); + + // Return the number of elements in a collection. + function size(obj) { + if (obj == null) return 0; + return isArrayLike(obj) ? obj.length : keys(obj).length; + } + + // Internal `_.pick` helper function to determine whether `key` is an enumerable + // property name of `obj`. + function keyInObj(value, key, obj) { + return key in obj; + } + + // Return a copy of the object only containing the allowed properties. + var pick = restArguments(function(obj, keys) { + var result = {}, iteratee = keys[0]; + if (obj == null) return result; + if (isFunction$1(iteratee)) { + if (keys.length > 1) iteratee = optimizeCb(iteratee, keys[1]); + keys = allKeys(obj); + } else { + iteratee = keyInObj; + keys = flatten$1(keys, false, false); + obj = Object(obj); + } + for (var i = 0, length = keys.length; i < length; i++) { + var key = keys[i]; + var value = obj[key]; + if (iteratee(value, key, obj)) result[key] = value; + } + return result; + }); + + // Return a copy of the object without the disallowed properties. + var omit = restArguments(function(obj, keys) { + var iteratee = keys[0], context; + if (isFunction$1(iteratee)) { + iteratee = negate(iteratee); + if (keys.length > 1) context = keys[1]; + } else { + keys = map(flatten$1(keys, false, false), String); + iteratee = function(value, key) { + return !contains(keys, key); + }; + } + return pick(obj, iteratee, context); + }); + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. + function initial(array, n, guard) { + return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); + } + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. The **guard** check allows it to work with `_.map`. + function first(array, n, guard) { + if (array == null || array.length < 1) return n == null || guard ? void 0 : []; + if (n == null || guard) return array[0]; + return initial(array, array.length - n); + } + + // Returns everything but the first entry of the `array`. Especially useful on + // the `arguments` object. Passing an **n** will return the rest N values in the + // `array`. + function rest(array, n, guard) { + return slice.call(array, n == null || guard ? 1 : n); + } + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. + function last(array, n, guard) { + if (array == null || array.length < 1) return n == null || guard ? void 0 : []; + if (n == null || guard) return array[array.length - 1]; + return rest(array, Math.max(0, array.length - n)); + } + + // Trim out all falsy values from an array. + function compact(array) { + return filter(array, Boolean); + } + + // Flatten out an array, either recursively (by default), or up to `depth`. + // Passing `true` or `false` as `depth` means `1` or `Infinity`, respectively. + function flatten(array, depth) { + return flatten$1(array, depth, false); + } + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + var difference = restArguments(function(array, rest) { + rest = flatten$1(rest, true, true); + return filter(array, function(value){ + return !contains(rest, value); + }); + }); + + // Return a version of the array that does not contain the specified value(s). + var without = restArguments(function(array, otherArrays) { + return difference(array, otherArrays); + }); + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // The faster algorithm will not work with an iteratee if the iteratee + // is not a one-to-one function, so providing an iteratee will disable + // the faster algorithm. + function uniq(array, isSorted, iteratee, context) { + if (!isBoolean(isSorted)) { + context = iteratee; + iteratee = isSorted; + isSorted = false; + } + if (iteratee != null) iteratee = cb(iteratee, context); + var result = []; + var seen = []; + for (var i = 0, length = getLength(array); i < length; i++) { + var value = array[i], + computed = iteratee ? iteratee(value, i, array) : value; + if (isSorted && !iteratee) { + if (!i || seen !== computed) result.push(value); + seen = computed; + } else if (iteratee) { + if (!contains(seen, computed)) { + seen.push(computed); + result.push(value); + } + } else if (!contains(result, value)) { + result.push(value); + } + } + return result; + } + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + var union = restArguments(function(arrays) { + return uniq(flatten$1(arrays, true, true)); + }); + + // Produce an array that contains every item shared between all the + // passed-in arrays. + function intersection(array) { + var result = []; + var argsLength = arguments.length; + for (var i = 0, length = getLength(array); i < length; i++) { + var item = array[i]; + if (contains(result, item)) continue; + var j; + for (j = 1; j < argsLength; j++) { + if (!contains(arguments[j], item)) break; + } + if (j === argsLength) result.push(item); + } + return result; + } + + // Complement of zip. Unzip accepts an array of arrays and groups + // each array's elements on shared indices. + function unzip(array) { + var length = (array && max(array, getLength).length) || 0; + var result = Array(length); + + for (var index = 0; index < length; index++) { + result[index] = pluck(array, index); + } + return result; + } + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + var zip = restArguments(unzip); + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. Passing by pairs is the reverse of `_.pairs`. + function object(list, values) { + var result = {}; + for (var i = 0, length = getLength(list); i < length; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + } + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](https://docs.python.org/library/functions.html#range). + function range(start, stop, step) { + if (stop == null) { + stop = start || 0; + start = 0; + } + if (!step) { + step = stop < start ? -1 : 1; + } + + var length = Math.max(Math.ceil((stop - start) / step), 0); + var range = Array(length); + + for (var idx = 0; idx < length; idx++, start += step) { + range[idx] = start; + } + + return range; + } + + // Chunk a single array into multiple arrays, each containing `count` or fewer + // items. + function chunk(array, count) { + if (count == null || count < 1) return []; + var result = []; + var i = 0, length = array.length; + while (i < length) { + result.push(slice.call(array, i, i += count)); + } + return result; + } + + // Helper function to continue chaining intermediate results. + function chainResult(instance, obj) { + return instance._chain ? _$1(obj).chain() : obj; + } + + // Add your own custom functions to the Underscore object. + function mixin(obj) { + each(functions(obj), function(name) { + var func = _$1[name] = obj[name]; + _$1.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return chainResult(this, func.apply(_$1, args)); + }; + }); + return _$1; + } + + // Add all mutator `Array` functions to the wrapper. + each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _$1.prototype[name] = function() { + var obj = this._wrapped; + if (obj != null) { + method.apply(obj, arguments); + if ((name === 'shift' || name === 'splice') && obj.length === 0) { + delete obj[0]; + } + } + return chainResult(this, obj); + }; + }); + + // Add all accessor `Array` functions to the wrapper. + each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _$1.prototype[name] = function() { + var obj = this._wrapped; + if (obj != null) obj = method.apply(obj, arguments); + return chainResult(this, obj); + }; + }); + + // Named Exports + + var allExports = { + __proto__: null, + VERSION: VERSION, + restArguments: restArguments, + isObject: isObject, + isNull: isNull, + isUndefined: isUndefined, + isBoolean: isBoolean, + isElement: isElement, + isString: isString, + isNumber: isNumber, + isDate: isDate, + isRegExp: isRegExp, + isError: isError, + isSymbol: isSymbol, + isArrayBuffer: isArrayBuffer, + isDataView: isDataView$1, + isArray: isArray, + isFunction: isFunction$1, + isArguments: isArguments$1, + isFinite: isFinite$1, + isNaN: isNaN$1, + isTypedArray: isTypedArray$1, + isEmpty: isEmpty, + isMatch: isMatch, + isEqual: isEqual, + isMap: isMap, + isWeakMap: isWeakMap, + isSet: isSet, + isWeakSet: isWeakSet, + keys: keys, + allKeys: allKeys, + values: values, + pairs: pairs, + invert: invert, + functions: functions, + methods: functions, + extend: extend, + extendOwn: extendOwn, + assign: extendOwn, + defaults: defaults, + create: create, + clone: clone, + tap: tap, + get: get, + has: has, + mapObject: mapObject, + identity: identity, + constant: constant, + noop: noop, + toPath: toPath$1, + property: property, + propertyOf: propertyOf, + matcher: matcher, + matches: matcher, + times: times, + random: random, + now: now, + escape: _escape, + unescape: _unescape, + templateSettings: templateSettings, + template: template, + result: result, + uniqueId: uniqueId, + chain: chain, + iteratee: iteratee, + partial: partial, + bind: bind, + bindAll: bindAll, + memoize: memoize, + delay: delay, + defer: defer, + throttle: throttle, + debounce: debounce, + wrap: wrap, + negate: negate, + compose: compose, + after: after, + before: before, + once: once, + findKey: findKey, + findIndex: findIndex, + findLastIndex: findLastIndex, + sortedIndex: sortedIndex, + indexOf: indexOf, + lastIndexOf: lastIndexOf, + find: find, + detect: find, + findWhere: findWhere, + each: each, + forEach: each, + map: map, + collect: map, + reduce: reduce, + foldl: reduce, + inject: reduce, + reduceRight: reduceRight, + foldr: reduceRight, + filter: filter, + select: filter, + reject: reject, + every: every, + all: every, + some: some, + any: some, + contains: contains, + includes: contains, + include: contains, + invoke: invoke, + pluck: pluck, + where: where, + max: max, + min: min, + shuffle: shuffle, + sample: sample, + sortBy: sortBy, + groupBy: groupBy, + indexBy: indexBy, + countBy: countBy, + partition: partition, + toArray: toArray, + size: size, + pick: pick, + omit: omit, + first: first, + head: first, + take: first, + initial: initial, + last: last, + rest: rest, + tail: rest, + drop: rest, + compact: compact, + flatten: flatten, + without: without, + uniq: uniq, + unique: uniq, + union: union, + intersection: intersection, + difference: difference, + unzip: unzip, + transpose: unzip, + zip: zip, + object: object, + range: range, + chunk: chunk, + mixin: mixin, + 'default': _$1 + }; + + // Default Export + + // Add all of the Underscore functions to the wrapper object. + var _ = mixin(allExports); + // Legacy Node.js API. + _._ = _; + + return _; + +}))); +//# sourceMappingURL=underscore-umd.js.map diff --git a/creme/settings.py b/creme/settings.py index 06ee4e5d56..406ba84450 100644 --- a/creme/settings.py +++ b/creme/settings.py @@ -833,7 +833,7 @@ {'filter': 'mediagenerator.filters.media_url.MediaURL'}, 'creme_core/js/media.js', - 'creme_core/js/lib/underscore/underscore-1.13.2.js', + 'creme_core/js/lib/underscore/underscore-1.13.7.js', 'creme_core/js/jquery/3.x/jquery-3.7.1.js', 'creme_core/js/jquery/3.x/jquery-migrate-3.4.1.js', 'creme_core/js/jquery/ui/jquery-ui-1.13.1.js', From c9c561e3b00be62235e94b6cc09eb0d46f12c6dd Mon Sep 17 00:00:00 2001 From: Fabre Florian Date: Fri, 24 Jan 2025 10:17:59 +0100 Subject: [PATCH 17/29] Drop some untested compatibily code for old browser in Lambda --- .../static/creme_core/js/widgets/utils/lambda.js | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/creme/creme_core/static/creme_core/js/widgets/utils/lambda.js b/creme/creme_core/static/creme_core/js/widgets/utils/lambda.js index 1e30509afb..9c97ab6456 100644 --- a/creme/creme_core/static/creme_core/js/widgets/utils/lambda.js +++ b/creme/creme_core/static/creme_core/js/widgets/utils/lambda.js @@ -73,19 +73,8 @@ creme.utils.Lambda = creme.component.Component.sub({ parameters = Array.isArray(parameters) ? parameters.join(',') : (parameters || ''); var body = callable.indexOf('return') !== -1 ? callable : 'return ' + callable + ';'; - /* eslint-disable no-new-func, no-eval */ - if (!Object.isNone(window['Function'])) { - this._lambda = new Function(parameters, body); - } else { - // HACK : compatibility for older browsers - var uuid = _.uniqueId('__lambda__'); - - eval('creme.utils["' + uuid + '"] = function(' + parameters + ') {' + body + "};"); - - this._lambda = creme.utils[uuid]; - delete creme.utils[uuid]; - } - /* eslint-enable no-new-func, no-eval */ + // eslint-disable-next-line no-new-func, no-eval + this._lambda = new Function(parameters, body); return this; }, From 403ae28ddcdd7af12168ad0aac349c1951a6cc67 Mon Sep 17 00:00:00 2001 From: joehybird Date: Wed, 22 Jan 2025 15:00:05 +0100 Subject: [PATCH 18/29] Drop jquery.form; Use FormData instead. --- .eslintrc | 1 + CHANGELOG.txt | 8 + .../js/tests/custom-forms-brick.js | 10 +- .../creme_core/static/creme_core/js/bricks.js | 10 +- .../creme_core/static/creme_core/js/forms.js | 17 +- .../creme_core/js/lib/underscore/object.js | 36 ++++ .../static/creme_core/js/list_view.core.js | 9 +- .../creme_core/js/tests/ajax/backend.js | 58 +++--- .../js/tests/ajax/qunit-ajax-mixin.js | 10 + .../static/creme_core/js/tests/ajax/utils.js | 164 +++++++++++++++-- .../js/tests/brick/brick-actions.js | 48 ++++- .../creme_core/js/tests/list/listview-core.js | 11 +- .../creme_core/js/tests/underscore/object.js | 25 +++ .../creme_core/js/widgets/ajax/backend.js | 171 ++++++++++++++---- .../creme_core/js/widgets/ajax/mockbackend.js | 24 ++- .../creme_core/js/widgets/ajax/query.js | 6 +- creme/settings.py | 7 +- 17 files changed, 494 insertions(+), 121 deletions(-) create mode 100644 creme/creme_core/static/creme_core/js/lib/underscore/object.js create mode 100644 creme/creme_core/static/creme_core/js/tests/underscore/object.js diff --git a/.eslintrc b/.eslintrc index 7c2a177411..1b2fb20518 100644 --- a/.eslintrc +++ b/.eslintrc @@ -27,6 +27,7 @@ "Blob": true, "Set": true, "ResizeObserver": true, + "FormData": true, /* External libraries */ "jQuery": true, diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 967d4c96e3..adf5095c0d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -190,6 +190,8 @@ - Form submit error responses with HTML can either replace the default overlay content or the frame content. - New creme.dialog.Frame option 'fillOnError'; if enabled the html error response replaces the content. - New creme.dialog.Dialog option 'fillFrameOnError'; if enabled the html error response replaces the content. + * creme.ajax.jqueryAjaxSend() arguments have changed from (url, data, successCb, errorCb, options) to ({url: url, body: data, ...options}, {done: successCb, fail: errorCb}). + * Added _.pop() method that works like python dict.pop(). Breaking changes : ------------------ @@ -245,6 +247,12 @@ - Object.entries() - Object.getPrototypeOf() * Drop creme.widget.template(); Use String.prototype.template instead. + * Refactor of creme.ajax.Backend: + - Drop jquery.forms plugin; Use FormData standard API instead. + - $.validateHTML5() is removed (came along jquery.forms) + - Add support for download & upload progress event : new 'uploadProgress' & 'progress' options. + - Refactor 'ListViewController' & 'creme.bricks.BricksReloader' + - 'creme.ajax.Query' now accepts new 'uploadProgress' & 'progress' options. - Apps : * Activities: - Refactor creme.ActivityCalendar : can be now used as independent component. diff --git a/creme/creme_config/static/creme_config/js/tests/custom-forms-brick.js b/creme/creme_config/static/creme_config/js/tests/custom-forms-brick.js index 98abb60ef4..06ac9a95fd 100644 --- a/creme/creme_config/static/creme_config/js/tests/custom-forms-brick.js +++ b/creme/creme_config/static/creme_config/js/tests/custom-forms-brick.js @@ -326,7 +326,10 @@ QUnit.test('creme.FormGroupsController (reorder groups)', function(assert) { {brick_id: ['creme_core-test'], extra_data: '{}'}, {dataType: 'json', delay: 0, enableUriSearch: false, sync: true} ] - ], this.mockBackendCalls()); + ], this.mockBackendCalls().map(function(e) { + var request = _.omit(e[3], 'progress'); + return [e[0], e[1], e[2], request]; + })); }); @@ -382,7 +385,10 @@ QUnit.test('creme.FormGroupsController (reorder groups, failure)', function(asse {"brick_id": ["creme_core-test"], "extra_data": "{}"}, {dataType: "json", delay: 0, enableUriSearch: false, sync: true} ] - ], this.mockBackendCalls()); + ], this.mockBackendCalls().map(function(e) { + var request = _.omit(e[3], 'progress'); + return [e[0], e[1], e[2], request]; + })); }); QUnit.test('creme.FormGroupsController (toggle item)', function(assert) { diff --git a/creme/creme_core/static/creme_core/js/bricks.js b/creme/creme_core/static/creme_core/js/bricks.js index f41626b21a..223c914e6a 100644 --- a/creme/creme_core/static/creme_core/js/bricks.js +++ b/creme/creme_core/static/creme_core/js/bricks.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2015-2024 Hybird + Copyright (C) 2015-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -1021,12 +1021,8 @@ creme.bricks.BricksReloader.prototype = { sync: false, dataType: 'json' }, - onDownloadProgress: function(evt) { - var percent = 100; - - if (evt.lengthComputable && evt.total > 0) { - percent = Math.trunc(Math.max((evt.loaded / evt.total) * 100, 0) / 20) * 20; - } + progress: function(evt) { + var percent = _.clamp(evt.loadedPercent || 100, 20, 100); bricks.forEach(function(brick) { brick.setDownloadStatus(percent); diff --git a/creme/creme_core/static/creme_core/js/forms.js b/creme/creme_core/static/creme_core/js/forms.js index d5863c6761..dd7276d727 100644 --- a/creme/creme_core/static/creme_core/js/forms.js +++ b/creme/creme_core/static/creme_core/js/forms.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2009-2023 Hybird + Copyright (C) 2009-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -192,6 +192,19 @@ creme.forms.toImportField = function(table_id, target_query, speed) { $csv_select.on('change', handleColChange); }; +// Backport from jquery.form-3.51 +// TODO : factorize code in form controller +function __validateHTML5(element) { + var errors = {}; + + $('*:invalid', element).each(function(index, item) { + errors[$(this).prop('name')] = item.validationMessage; + }); + + return errors; +} + + // TODO : create a real form controller with better lifecycle (not just a css class) and // factorize some code with creme.dialog.FormDialog for html5 validation. creme.forms.initialize = function(form) { @@ -215,7 +228,7 @@ creme.forms.initialize = function(form) { form.attr('novalidate', 'novalidate'); } - var isHtml5Valid = Object.isEmpty(form.validateHTML5()); + var isHtml5Valid = Object.isEmpty(__validateHTML5(form)); if (isHtml5Valid === true) { if (button.is(':not(.is-form-submit)')) { diff --git a/creme/creme_core/static/creme_core/js/lib/underscore/object.js b/creme/creme_core/static/creme_core/js/lib/underscore/object.js new file mode 100644 index 0000000000..731a940317 --- /dev/null +++ b/creme/creme_core/static/creme_core/js/lib/underscore/object.js @@ -0,0 +1,36 @@ +/******************************************************************************* + Creme is a free/open-source Customer Relationship Management software + Copyright (C) 2025 Hybird + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*******************************************************************************/ + +(function() { +"use strict"; + +function pop(object, name, defaults) { + if (object instanceof Object && name in object) { + var value = object[name]; + delete object[name]; + return value; + } else { + return defaults; + } +} + +_.mixin({ + pop: pop +}); + +}()); diff --git a/creme/creme_core/static/creme_core/js/list_view.core.js b/creme/creme_core/static/creme_core/js/list_view.core.js index bed55734b9..43ba42d839 100644 --- a/creme/creme_core/static/creme_core/js/list_view.core.js +++ b/creme/creme_core/static/creme_core/js/list_view.core.js @@ -572,13 +572,8 @@ var queryData = $.extend({}, state, {content: 1}); var queryOptions = { action: 'POST', - onDownloadProgress: function(evt) { - var percent = 100; - - if (evt.lengthComputable && evt.total > 0) { - percent = Math.trunc(Math.max((evt.loaded / evt.total) * 100, 0) / 20) * 20; - } - + progress: function(evt) { + var percent = _.clamp(evt.loadedPercent || 100, 20, 100); self._updateLoadingProgress(percent); } }; diff --git a/creme/creme_core/static/creme_core/js/tests/ajax/backend.js b/creme/creme_core/static/creme_core/js/tests/ajax/backend.js index fee7e17b25..5e1e31ef12 100644 --- a/creme/creme_core/static/creme_core/js/tests/ajax/backend.js +++ b/creme/creme_core/static/creme_core/js/tests/ajax/backend.js @@ -2,8 +2,8 @@ (function($) { -QUnit.module("creme.ajax.utils.js", new QUnitMixin(QUnitAjaxMixin, - QUnitEventMixin, { +QUnit.module("creme.ajax.Backend", new QUnitMixin(QUnitAjaxMixin, + QUnitEventMixin, { afterEach: function() { // reset csrftoken cookie document.cookie = 'csrftoken=;expires=Thu, 01 Jan 1970 00:00:00 GMT'; @@ -41,7 +41,7 @@ QUnit.parametrize('creme.ajax.Backend.get', [ backend.get(url, data, successCb, errorCb, queryOptions); }); - // retrieve internal callbacks from the ajaxSubmit call + // retrieve internal callbacks from the $.ajax call var ajaxCall = ajaxFaker.calls()[0][0]; ok(Object.isFunc(ajaxCall.success)); ok(Object.isFunc(ajaxCall.error)); @@ -76,7 +76,7 @@ QUnit.parametrize('creme.ajax.Backend.post', [ backend.post(url, data, successCb, errorCb, queryOptions); }); - // retrieve internal callbacks from the ajaxSubmit call + // retrieve internal callbacks from the $.ajax call var ajaxCall = ajaxFaker.calls()[0][0]; ok(Object.isFunc(ajaxCall.success)); ok(Object.isFunc(ajaxCall.error)); @@ -91,7 +91,6 @@ QUnit.parametrize('creme.ajax.Backend.post', [ QUnit.parametrize('creme.ajax.Backend.submit', [ [{}, {}, { - iframe: true, traditional: true, url: 'mock/a', data: {}, @@ -100,7 +99,6 @@ QUnit.parametrize('creme.ajax.Backend.submit', [ } }], [{traditional: false}, {}, { - iframe: true, traditional: false, url: 'mock/a', data: {}, @@ -109,7 +107,6 @@ QUnit.parametrize('creme.ajax.Backend.submit', [ } }], [{headers: {'X-Client-Id': 'my-key'}}, {action: 'mock/b'}, { - iframe: true, traditional: true, url: 'mock/b', data: {}, @@ -119,7 +116,6 @@ QUnit.parametrize('creme.ajax.Backend.submit', [ } }], [{action: 'mock/b'}, {headers: {'X-Client-Id': 'my-key'}}, { - iframe: true, traditional: true, url: 'mock/b', data: {}, @@ -129,10 +125,11 @@ QUnit.parametrize('creme.ajax.Backend.submit', [ } }], [{}, {data: {any: 'value'}, headers: {'X-Client-Id': 'my-key'}}, { - iframe: true, traditional: true, url: 'mock/a', data: { + text: 'A', + file: null, any: 'value' }, headers: { @@ -143,8 +140,8 @@ QUnit.parametrize('creme.ajax.Backend.submit', [ ], function(backendOptions, queryOptions, expected, assert) { var successCb = function() {}; var errorCb = function() {}; - var submitFaker = new FunctionFaker({ - instance: $.fn, method: 'ajaxSubmit' + var ajaxFaker = new FunctionFaker({ + instance: $, method: 'ajax' }); document.cookie = 'csrftoken=my-token'; @@ -156,22 +153,21 @@ QUnit.parametrize('creme.ajax.Backend.submit', [ '' ); - submitFaker.with(function() { + ajaxFaker.with(function() { var backend = new creme.ajax.Backend(backendOptions); backend.submit(form, successCb, errorCb, queryOptions); }); - // retrieve internal callbacks from the ajaxSubmit call - var submitCall = submitFaker.calls()[0][0]; - ok(Object.isFunc(submitCall.success)); - ok(Object.isFunc(submitCall.error)); + // retrieve internal callbacks from the $.ajax call + var ajaxCall = ajaxFaker.calls()[0][0]; + ok(Object.isFunc(ajaxCall.success)); + ok(Object.isFunc(ajaxCall.error)); - equal(form.attr('action'), expected.url); + equal(ajaxCall.url, expected.url); - equal(submitCall.traditional, expected.traditional); - equal(submitCall.iframe, expected.iframe); - deepEqual(submitCall.data || {}, expected.data); - deepEqual(submitCall.headers, expected.headers); + equal(ajaxCall.traditional, expected.traditional); + deepEqual(ajaxCall.data || new FormData(), this.createFormData(expected.data)); + deepEqual(ajaxCall.headers, expected.headers); }); QUnit.parametrize('creme.ajax.Backend (debug)', [ @@ -180,9 +176,6 @@ QUnit.parametrize('creme.ajax.Backend (debug)', [ ], function(isDebug, expected, assert) { var successCb = function() {}; var errorCb = function() {}; - var submitFaker = new FunctionFaker({ - instance: $.fn, method: 'ajaxSubmit' - }); var ajaxFaker = new FunctionFaker({ instance: $, method: 'ajax' }); @@ -195,19 +188,16 @@ QUnit.parametrize('creme.ajax.Backend (debug)', [ ); logFaker.with(function() { - submitFaker.with(function() { - ajaxFaker.with(function() { - var backend = new creme.ajax.Backend({debug: isDebug}); - - backend.get('mock/a', {}, successCb, errorCb, {}); - backend.post('mock/a', {}, successCb, errorCb, {}); - backend.submit(form, successCb, errorCb, {}); - }); + ajaxFaker.with(function() { + var backend = new creme.ajax.Backend({debug: isDebug}); + + backend.get('mock/a', {}, successCb, errorCb, {}); + backend.post('mock/a', {}, successCb, errorCb, {}); + backend.submit(form, successCb, errorCb, {}); }); }); - equal(submitFaker.count(), 1); - equal(ajaxFaker.count(), 2); + equal(ajaxFaker.count(), 3); equal(logFaker.count(), expected); }); diff --git a/creme/creme_core/static/creme_core/js/tests/ajax/qunit-ajax-mixin.js b/creme/creme_core/static/creme_core/js/tests/ajax/qunit-ajax-mixin.js index 0708a48789..00ddd97ee1 100644 --- a/creme/creme_core/static/creme_core/js/tests/ajax/qunit-ajax-mixin.js +++ b/creme/creme_core/static/creme_core/js/tests/ajax/qunit-ajax-mixin.js @@ -140,6 +140,16 @@ mockHistoryChanges: function() { return this._historyChanges; + }, + + createFormData: function(data) { + var formdata = new FormData(); + + for (var key in data) { + formdata.append(key, data[key]); + } + + return formdata; } }; }(jQuery)); diff --git a/creme/creme_core/static/creme_core/js/tests/ajax/utils.js b/creme/creme_core/static/creme_core/js/tests/ajax/utils.js index d4636c3b52..530709f28d 100644 --- a/creme/creme_core/static/creme_core/js/tests/ajax/utils.js +++ b/creme/creme_core/static/creme_core/js/tests/ajax/utils.js @@ -1,4 +1,4 @@ -/* globals FunctionFaker */ +/* globals FunctionFaker ProgressEvent */ (function($) { @@ -277,7 +277,6 @@ QUnit.test('creme.ajax.cookieCSRF', function(assert) { document.cookie = 'csrftoken=z56ZnN90D1eeah7roE5'; equal("z56ZnN90D1eeah7roE5", creme.ajax.cookieCSRF()); - } finally { if (csrftoken) { document.cookie = 'csrftoken=' + csrftoken; @@ -286,7 +285,7 @@ QUnit.test('creme.ajax.cookieCSRF', function(assert) { } } }); - +/* QUnit.parameterize('creme.ajax.jqueryFormSubmit (url)', [ ['', {}, { action: undefined @@ -578,7 +577,7 @@ QUnit.test('creme.ajax.jqueryFormSubmit (no callback)', function(assert) { submitCall.success('HTTPError 403', 'success', {status: 0, responseText: "HTTPError 403"}, form); submitCall.success('Ok', 'success', {status: 0, responseText: "Ok"}, form); }); - +*/ QUnit.parameterize('creme.ajax.jqueryAjaxSend (options)', [ ['', {}, {}, { url: '', @@ -603,16 +602,25 @@ QUnit.parameterize('creme.ajax.jqueryAjaxSend (options)', [ dataType: 'text', type: 'POST', headers: {} + }], + ['mock/a', {a: 12}, {method: 'POST', dataType: 'text', extraData: {b: 6}}, { + url: 'mock/a', + async: true, + data: {a: 12, b: 6}, + dataType: 'text', + type: 'POST', + headers: {} }] ], function(url, data, options, expected, assert) { - var successCb = function() {}; - var errorCb = function() {}; var ajaxFaker = new FunctionFaker({ instance: $, method: 'ajax' }); ajaxFaker.with(function() { - creme.ajax.jqueryAjaxSend(url, data, successCb, errorCb, options); + creme.ajax.jqueryAjaxSend(Object.assign({ + url: url, + data: data + }, options)); }); equal(ajaxFaker.count(), 1); @@ -628,18 +636,22 @@ QUnit.parameterize('creme.ajax.jqueryAjaxSend (options)', [ }); QUnit.parameterize('creme.ajax.jqueryAjaxSend (headers)', [ - ['my-token', {}, { - headers: {'X-CSRFToken': 'my-token'} + ['', {csrf: 'my-query-token'}, { + headers: {'X-CSRFToken': 'my-query-token'} + }], + ['', {csrf: true}, { + headers: {} + }], + ['', {headers: {'X-CSRFToken': 'my-query-token'}}, { + headers: {'X-CSRFToken': 'my-query-token'} }], - ['', {headers: {'X-CSRFToken': 'my-token'}}, { + ['my-token', {csrf: true}, { headers: {'X-CSRFToken': 'my-token'} }], ['my-token', {headers: {'X-CSRFToken': 'my-other-token'}}, { headers: {'X-CSRFToken': 'my-other-token'} }] ], function(token, options, expected, assert) { - var successCb = function() {}; - var errorCb = function() {}; var ajaxFaker = new FunctionFaker({ instance: $, method: 'ajax' }); @@ -649,7 +661,9 @@ QUnit.parameterize('creme.ajax.jqueryAjaxSend (headers)', [ document.cookie = 'csrftoken=' + token; } - creme.ajax.jqueryAjaxSend('mock/a', {}, successCb, errorCb, options); + creme.ajax.jqueryAjaxSend(Object.assign({ + url: 'mock/a' + }, options || {})); }); equal(ajaxFaker.count(), 1); @@ -681,10 +695,15 @@ QUnit.parameterize('creme.ajax.jqueryAjaxSend (error callback)', [ }); ajaxFaker.with(function() { - creme.ajax.jqueryAjaxSend('mock/a', {}, successCb.wrap(), errorCb.wrap(), {}); + creme.ajax.jqueryAjaxSend({ + url: 'mock/a' + }, { + done: successCb.wrap(), + fail: errorCb.wrap() + }); }); - // retrieve internal callbacks from the ajaxSubmit call + // retrieve internal callbacks from the $.ajax call var ajaxCall = ajaxFaker.calls()[0][0]; ok(Object.isFunc(ajaxCall.success)); ok(Object.isFunc(ajaxCall.error)); @@ -715,10 +734,15 @@ QUnit.test('creme.ajax.jqueryAjaxSend (success callback)', function(assert) { }); ajaxFaker.with(function() { - creme.ajax.jqueryAjaxSend('mock/a', {}, successCb.wrap(), errorCb.wrap(), {}); + creme.ajax.jqueryAjaxSend({ + url: 'mock/a' + }, { + done: successCb.wrap(), + fail: errorCb.wrap() + }); }); - // retrieve internal callbacks from the ajaxSubmit call + // retrieve internal callbacks from the $.ajax call var ajaxCall = ajaxFaker.calls()[0][0]; ok(Object.isFunc(ajaxCall.success)); ok(Object.isFunc(ajaxCall.error)); @@ -740,10 +764,12 @@ QUnit.test('creme.ajax.jqueryAjaxSend (no callback)', function(assert) { }); ajaxFaker.with(function() { - creme.ajax.jqueryAjaxSend('mock/a', {}, undefined, undefined, {}); + creme.ajax.jqueryAjaxSend({ + url: 'mock/a' + }); }); - // retrieve internal callbacks from the ajaxSubmit call + // retrieve internal callbacks from the $.ajax call var ajaxCall = ajaxFaker.calls()[0][0]; ok(Object.isFunc(ajaxCall.success)); ok(Object.isFunc(ajaxCall.error)); @@ -754,4 +780,104 @@ QUnit.test('creme.ajax.jqueryAjaxSend (no callback)', function(assert) { ajaxCall.success('Ok', 'success', {status: 200, responseText: "Ok"}); }); +QUnit.test('creme.ajax.jqueryAjaxSend (legacy callbacks)', function(assert) { + var successCb = new FunctionFaker(); + var errorCb = new FunctionFaker(); + var ajaxFaker = new FunctionFaker({ + instance: $, method: 'ajax' + }); + + ajaxFaker.with(function() { + creme.ajax.jqueryAjaxSend({ + url: 'mock/a', + success: successCb.wrap(), + error: errorCb.wrap() + }); + }); + + // retrieve internal callbacks from the $.ajax call + var ajaxCall = ajaxFaker.calls()[0][0]; + ok(Object.isFunc(ajaxCall.success)); + ok(Object.isFunc(ajaxCall.error)); + + // now call internal error callback + ajaxCall.error({status: 400, responseText: "Wrong call!", statusText: 'error'}, 'error'); + ajaxCall.error({status: 0, responseText: "JSON error"}, 'parseerror'); + ajaxCall.success('Ok', 'success', {status: 200, responseText: "Ok"}); + + deepEqual(errorCb.calls(), [ + ['Wrong call!', { + type: 'request', + status: 400, + message: 'HTTP 400 - error', + request: {status: 400, statusText: 'error', responseText: "Wrong call!"} + }], + ['JSON error', { + type: 'request', + status: 0, + message: 'JSON parse error', + request: {status: 0, responseText: "JSON error"} + }] + ]); + + deepEqual(successCb.calls(), [ + ['Ok', 'success', {status: 200, responseText: 'Ok'}] + ]); +}); + +QUnit.parameterize('creme.ajax.jqueryAjaxSend (progress)', [ + [ + {lengthComputable: false, loaded: 1024, total: 4096}, + {lengthComputable: false, loaded: 1024, total: 4096, loadedPercent: 0} + ], + [ + {lengthComputable: true, loaded: 1024, total: 4096}, + {lengthComputable: true, loaded: 1024, total: 4096, loadedPercent: 25} + ], + [ + {lengthComputable: true, loaded: 4096, total: 4096}, + {lengthComputable: true, loaded: 4096, total: 4096, loadedPercent: 100} + ] +], function(state, expected, assert) { + var progressCb = new FunctionFaker(); + var uploadCb = new FunctionFaker(); + + var ajaxFaker = new FunctionFaker({ + instance: $, method: 'ajax' + }); + + ajaxFaker.with(function() { + creme.ajax.jqueryAjaxSend({ + url: 'mock/a' + }, { + progress: progressCb.wrap(), + uploadProgress: uploadCb.wrap() + }); + }); + + // retrieve internal callbacks from the $.ajax call + var ajaxCall = ajaxFaker.calls()[0][0]; + var xhr = ajaxCall.xhr(); + + xhr.dispatchEvent(new ProgressEvent('progress', state)); + xhr.upload.dispatchEvent(new ProgressEvent('progress', state)); + + equal(progressCb.count(), 1); + equal(uploadCb.count(), 1); + + function _progressEventData(args) { + var event = args[0]; + + return { + lengthComputable: event.lengthComputable, + loaded: event.loaded, + total: event.total, + loadedPercent: event.loadedPercent + }; + }; + + deepEqual(progressCb.calls().map(_progressEventData), [expected]); + deepEqual(uploadCb.calls().map(_progressEventData), [expected]); +}); + }(jQuery)); diff --git a/creme/creme_core/static/creme_core/js/tests/brick/brick-actions.js b/creme/creme_core/static/creme_core/js/tests/brick/brick-actions.js index ece253e864..09f6410d10 100644 --- a/creme/creme_core/static/creme_core/js/tests/brick/brick-actions.js +++ b/creme/creme_core/static/creme_core/js/tests/brick/brick-actions.js @@ -245,15 +245,49 @@ QUnit.test('creme.bricks.Brick.action (refresh)', function(assert) { deepEqual([['done']], this.mockListenerCalls('action-done').map(function(e) { return e.slice(0, 1); })); - deepEqual( - [[ - 'mock/brick/all/reload', 'GET', -// {"brick_id": ["brick-for-test"], "extra_data": "{}"}, - {"brick_id": ["creme_core-testA"], "extra_data": "{}"}, - {dataType: "json", delay: 0, enableUriSearch: false, sync: true} - ]], this.mockBackendCalls()); + deepEqual([[ + 'mock/brick/all/reload', 'GET', + {"brick_id": ["creme_core-testA"], "extra_data": "{}"}, + {dataType: "json", delay: 0, enableUriSearch: false, sync: true} + ]], this.mockBackendCalls().map(function(e) { + var request = _.omit(e[3], 'progress'); + return [e[0], e[1], e[2], request]; + })); +}); + +QUnit.test('creme.bricks.Brick.action (refresh, progress)', function(assert) { + this.backend.progressSteps = [15, 40, 50, 90]; + + var brick = this.createBrickWidget({id: 'creme_core-testA'}).brick(); + + brick.on('brick-loading-progress', this.mockListener('loading-progress')); + + brick.action('refresh').on(this.brickActionListeners).start(); + equal(false, brick.isLoading()); + + deepEqual([ + ['done'] + ], this.mockListenerCalls('action-done').map(function(e) { return e.slice(0, 1); })); + + deepEqual([ + ['brick-loading-progress', 20], + ['brick-loading-progress', 20], + ['brick-loading-progress', 40], + ['brick-loading-progress', 50], + ['brick-loading-progress', 90] + ], this.mockListenerCalls('loading-progress')); + + deepEqual([[ + 'mock/brick/all/reload', 'GET', + {"brick_id": ["creme_core-testA"], "extra_data": "{}"}, + {dataType: "json", delay: 0, enableUriSearch: false, sync: true} + ]], this.mockBackendCalls().map(function(e) { + var request = _.omit(e[3], 'progress'); + return [e[0], e[1], e[2], request]; + })); }); + QUnit.test('creme.bricks.Brick.action (add, submit)', function(assert) { // var brick = this.createBrickWidget('brick-for-test').brick(); var brick = this.createBrickWidget().brick(); diff --git a/creme/creme_core/static/creme_core/js/tests/list/listview-core.js b/creme/creme_core/static/creme_core/js/tests/list/listview-core.js index 902b0ffc9f..5742ac535a 100644 --- a/creme/creme_core/static/creme_core/js/tests/list/listview-core.js +++ b/creme/creme_core/static/creme_core/js/tests/list/listview-core.js @@ -481,6 +481,8 @@ QUnit.test('creme.listview.core (select all)', function(assert) { }); QUnit.test('creme.listview.core (submitState)', function(assert) { + this.backend.progressSteps = [15, 40, 50, 90]; + var html = this.createListViewHtml(this.defaultListViewHtmlOptions()); var element = $(html).appendTo(this.qunitFixture()); var listview = creme.widget.create(element); @@ -495,7 +497,8 @@ QUnit.test('creme.listview.core (submitState)', function(assert) { listview.controller().on('submit-state-start', this.mockListener('submit-state-start')) .on('submit-state-done', this.mockListener('submit-state-done')) - .on('submit-state-complete', this.mockListener('submit-state-complete')); + .on('submit-state-complete', this.mockListener('submit-state-complete')) + .on('submit-state-progress', this.mockListener('submit-state-progress')); listview.controller().submitState({custom_a: 12}, listener); @@ -542,6 +545,12 @@ QUnit.test('creme.listview.core (submitState)', function(assert) { deepEqual([ ['submit-state-complete', nextUrl.href(), html] ], this.mockListenerCalls('submit-state-complete')); + deepEqual([ + ['submit-state-progress', 20], + ['submit-state-progress', 40], + ['submit-state-progress', 50], + ['submit-state-progress', 90] + ], this.mockListenerCalls('submit-state-progress')); }); QUnit.test('creme.listview.core (submitState, already loading)', function(assert) { diff --git a/creme/creme_core/static/creme_core/js/tests/underscore/object.js b/creme/creme_core/static/creme_core/js/tests/underscore/object.js new file mode 100644 index 0000000000..cccba66632 --- /dev/null +++ b/creme/creme_core/static/creme_core/js/tests/underscore/object.js @@ -0,0 +1,25 @@ +(function() { + +QUnit.module("underscore-object", new QUnitMixin()); + +QUnit.test('pop', function(assert) { + var data = {a: 12, b: null, c: 'hello'}; + + equal(_.pop(data, 'other'), undefined); + equal(_.pop(data, 'other', 'mydefault'), 'mydefault'); + deepEqual(data, {a: 12, b: null, c: 'hello'}); + + equal(_.pop(data, 'a'), 12); + equal(_.pop(data, 'a'), undefined); + deepEqual(data, {b: null, c: 'hello'}); + + equal(_.pop(data, 'b'), null); + equal(_.pop(data, 'b'), undefined); + deepEqual(data, {c: 'hello'}); + + equal(_.pop(data, 'c'), 'hello'); + equal(_.pop(data, 'c', 'mydefault'), 'mydefault'); + deepEqual(data, {}); +}); + +}()); diff --git a/creme/creme_core/static/creme_core/js/widgets/ajax/backend.js b/creme/creme_core/static/creme_core/js/widgets/ajax/backend.js index b734f2f9fa..c7e902bfbf 100644 --- a/creme/creme_core/static/creme_core/js/widgets/ajax/backend.js +++ b/creme/creme_core/static/creme_core/js/widgets/ajax/backend.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2009-2024 Hybird + Copyright (C) 2009-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -44,34 +44,69 @@ creme.ajax.Backend = function(options) { }; creme.ajax.Backend.prototype = { - get: function(url, data, on_success, on_error, options) { - var opts = $.extend(true, {method: 'GET'}, this.options, options); + get: function(url, data, successCb, errorCb, options) { + var opts = $.extend(true, {}, this.options, options); + var debug = _.pop(opts, 'debug', false); - if (opts.debug) { + if (debug) { console.log('creme.ajax.Backend > GET', url, ' > data:', data, ', options:', opts); } - creme.ajax.jqueryAjaxSend(url, data, on_success, on_error, opts); + return creme.ajax.jqueryAjaxSend(Object.assign({ + url: url, + method: 'GET', + body: data + }, opts), { + done: successCb, + fail: errorCb + }); }, - post: function(url, data, on_success, on_error, options) { - var opts = $.extend(true, {method: 'POST'}, this.options, options); + post: function(url, data, successCb, errorCb, options) { + var opts = $.extend(true, {}, this.options, options); + var debug = _.pop(opts, 'debug', false); - if (opts.debug) { + if (debug) { console.log('creme.ajax.Backend > POST', url, ' > data:', data, ', options:', opts); } - creme.ajax.jqueryAjaxSend(url, data, on_success, on_error, opts); + return creme.ajax.jqueryAjaxSend(Object.assign({ + url: url, + method: 'POST', + body: data, + csrf: creme.ajax.cookieCSRF() + }, opts), { + done: successCb, + fail: errorCb + }); }, - submit: function(form, on_success, on_error, options) { + submit: function(form, successCb, errorCb, options) { var opts = $.extend(true, {}, this.options, options); + var formEl = form.get(0); + var debug = _.pop(opts, 'debug', false); - if (opts.debug) { + if (debug) { console.log('creme.ajax.Backend > SUBMIT', form.attr('action'), '> options:', opts); } - creme.ajax.jqueryFormSubmit(form, on_success, on_error, opts); + var url = _.pop(opts, 'url', _.pop(opts, 'action', form.attr('action'))) || ''; + var data = new FormData(formEl); + var extraData = _.pop(opts, 'data', opts.extraData || {}); + var csrf = data.get('csrfmiddlewaretoken') || creme.ajax.cookieCSRF(); + + return creme.ajax.jqueryAjaxSend(Object.assign({ + url: url, + method: 'POST', + body: data, + extraData: extraData, + csrf: csrf + }, opts), { + done: successCb, + fail: errorCb + }); + + // creme.ajax.jqueryFormSubmit(form, successCb, errorCb, opts); }, query: function(options) { @@ -168,7 +203,7 @@ creme.ajax.serializeFormAsDict = function(form, extraData) { return data; }; - +/* creme.ajax.jqueryFormSubmit = function(form, successCb, errorCb, options) { options = options || {}; @@ -235,54 +270,112 @@ creme.ajax.jqueryFormSubmit = function(form, successCb, errorCb, options) { $(form).ajaxSubmit(submitOptions); }; +*/ + +function xhrEventLoadedPercent(event) { + var position = event.loaded || event.position; /* event.position is deprecated */ + var total = event.total; + return (event.lengthComputable > 0 || event.lengthComputable === true) ? Math.ceil(position / total * 100) : 0; +} + +/* + * Workaround because jqXHR does not expose upload property + * https://github.com/jquery-form/form/blob/master/src/jquery.form.js#L401-L422 + */ +function progressXHR(listeners) { + listeners = listeners || {}; + + var xhr = $.ajaxSettings.xhr(); + + if (listeners.uploadProgress && xhr.upload) { + xhr.upload.addEventListener('progress', function(event) { + event.loadedPercent = xhrEventLoadedPercent(event); + listeners.uploadProgress(event); + }, false); + } + + if (listeners.progress) { + xhr.addEventListener('progress', function(event) { + event.loadedPercent = xhrEventLoadedPercent(event); + listeners.progress(event); + }, false); + } + + return xhr; +} + +function xhrErrorMessage(xhr, textStatus) { + if (textStatus === 'parseerror') { + return "JSON parse error"; + } else { + return "HTTP ${status} - ${statusText}".template(xhr); + } +}; + -// TODO : replace success_cb/error_cb by listeners. -creme.ajax.jqueryAjaxSend = function(url, data, successCb, errorCb, options) { +// TODO : Replace listeners by a Promise with 'uploadProgress' & 'progress' callbacks +creme.ajax.jqueryAjaxSend = function(options, listeners) { options = options || {}; + listeners = Object.assign({ + done: _.pop(options, 'success'), /* keeps compatibility with the old API */ + fail: _.pop(options, 'error'), + progress: _.pop(options, 'progress'), + uploadProgress: _.pop(options, 'uploadProgress') + }, listeners || {}); + + var csrf = options.csrf === true ? creme.ajax.cookieCSRF() : options.csrf; + var headers = Object.assign({}, options.headers || {}, Object.isEmpty(csrf) ? {} : {'X-CSRFToken': csrf}); + var method = (options.method || options.type || 'GET').toUpperCase(); + var body = (options.data || options.body); + var extraData = _.pop(options, 'extraData', {}); function _onSuccess(data, textStatus, xhr) { - if (Object.isFunc(successCb)) { - successCb(data, textStatus, xhr); - } - }; - - function _errorMessage(xhr, textStatus) { - if (textStatus === 'parseerror') { - return "JSON parse error"; - } else { - return "HTTP ${status} - ${statusText}".template(xhr); + if (Object.isFunc(listeners.done)) { + listeners.done(data, textStatus, xhr); } }; function _onError(xhr, textStatus, errorThrown) { - if (Object.isFunc(errorCb)) { - errorCb(xhr.responseText, { + if (Object.isFunc(listeners.fail)) { + listeners.fail(xhr.responseText, { type: "request", status: xhr.status, request: xhr, - message: _errorMessage(xhr, textStatus) + message: xhrErrorMessage(xhr, textStatus) }); } }; - var csrf = creme.ajax.cookieCSRF(); - var headers = {}; - - if (Object.isEmpty(csrf) === false) { - headers = {'X-CSRFToken': csrf}; - } - var ajaxOptions = $.extend(true, { - async: !options.sync, - type: options.method || 'GET', - url: url, - data: data || {}, + async: !_.pop(options, 'sync'), + type: method, + url: options.url, + data: body, dataType: options.dataType || 'json', headers: headers, success: _onSuccess, error: _onError }, options); + // uploadProgress callback needs a custom XHR instance to read the event. + if (listeners.uploadProgress || listeners.progress) { + ajaxOptions.xhr = function() { + return progressXHR(listeners); + }; + } + + // When body is FormData we have to disable all post processing from jquery + if (body instanceof FormData) { + ajaxOptions.processData = false; + ajaxOptions.contentType = false; + + for (var key in extraData) { + body.set(key, extraData[key]); + } + } else if (!Object.isEmpty(extraData)) { + ajaxOptions.data = Object.assign(body || {}, extraData); + } + return $.ajax(ajaxOptions); }; diff --git a/creme/creme_core/static/creme_core/js/widgets/ajax/mockbackend.js b/creme/creme_core/static/creme_core/js/widgets/ajax/mockbackend.js index b53fd44320..68aaedfdd2 100644 --- a/creme/creme_core/static/creme_core/js/widgets/ajax/mockbackend.js +++ b/creme/creme_core/static/creme_core/js/widgets/ajax/mockbackend.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2009-2022 Hybird + Copyright (C) 2009-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -16,6 +16,7 @@ along with this program. If not, see . *******************************************************************************/ +/* globals ProgressEvent */ (function($) { "use strict"; @@ -32,6 +33,7 @@ creme.ajax.MockAjaxBackend = function(options) { this.counts = {GET: 0, POST: 0, SUBMIT: 0}; this.parser = document.createElement('a'); + this.progressSteps = []; }; creme.ajax.MockAjaxBackend.prototype = new creme.ajax.Backend(); @@ -95,6 +97,26 @@ $.extend(creme.ajax.MockAjaxBackend.prototype, { var responseData = response.responseText; + if (options.updateProgress || options.progress) { + (this.progressSteps || []).forEach(function(step) { + var progressEvent = new ProgressEvent('progress', { + total: 1000, + loaded: _.clamp(step, 0, 100) * 10, + lengthComputable: true + }); + + progressEvent.loadedPercent = step; + + if (options.updateProgress) { + options.updateProgress(progressEvent); + } + + if (options.progress) { + options.progress(progressEvent); + } + }); + } + try { if (response.status === 200 && options.dataType === 'json') { responseData = JSON.parse(responseData); diff --git a/creme/creme_core/static/creme_core/js/widgets/ajax/query.js b/creme/creme_core/static/creme_core/js/widgets/ajax/query.js index 800772e928..ee12d9d2b7 100644 --- a/creme/creme_core/static/creme_core/js/widgets/ajax/query.js +++ b/creme/creme_core/static/creme_core/js/widgets/ajax/query.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2009-2013 Hybird + Copyright (C) 2009-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -98,10 +98,14 @@ creme.ajax.Query = creme.component.Action.sub({ options = $.extend(true, {}, this.options(), options || {}); var data = $.extend({}, this.data() || {}, options.data || {}); + // TODO : replace 'action' by 'method' var action = (options.action || 'get').toLowerCase(); var backendOptions = options.backend || {}; var url = this.url() || ''; + backendOptions.progress = _.pop(options, 'progress', backendOptions.progress); + backendOptions.uploadProgress = _.pop(options, 'uploadProgress', backendOptions.uploadProgress); + try { if (Object.isNone(this._backend)) { throw new Error('Missing ajax backend'); diff --git a/creme/settings.py b/creme/settings.py index 406ba84450..df78309d2d 100644 --- a/creme/settings.py +++ b/creme/settings.py @@ -839,7 +839,7 @@ 'creme_core/js/jquery/ui/jquery-ui-1.13.1.js', 'creme_core/js/jquery/ui/jquery-ui-locale.js', 'creme_core/js/jquery/extensions/jquery.dragtable.js', - 'creme_core/js/jquery/extensions/jquery.form-3.51.js', + # 'creme_core/js/jquery/extensions/jquery.form-3.51.js', 'creme_core/js/jquery/extensions/jquery.floatthead-2.2.4.js', 'creme_core/js/lib/momentjs/moment-2.29.4.js', 'creme_core/js/lib/momentjs/locale/en-us.js', @@ -859,6 +859,9 @@ # jQuery tools 'creme_core/js/jquery/extensions/jquery.toggle-attr.js', + # Underscore tools + 'creme_core/js/lib/underscore/object.js', + # Base tools 'creme_core/js/lib/fallbacks/object-0.1.js', 'creme_core/js/lib/fallbacks/array-0.9.js', @@ -1052,6 +1055,7 @@ 'testcore.js', 'creme_core/js/tests/jquery/toggle-attr.js', + 'creme_core/js/tests/underscore/object.js', # Content 'creme_core/js/tests/component/component.js', @@ -1071,6 +1075,7 @@ 'creme_core/js/tests/ajax/query.js', 'creme_core/js/tests/ajax/localize.js', 'creme_core/js/tests/ajax/utils.js', + 'creme_core/js/tests/ajax/backend.js', 'creme_core/js/tests/model/collection.js', 'creme_core/js/tests/model/renderer-list.js', From 3dbb75eb9a5d1d664a2ce8d746ae36f9247c1213 Mon Sep 17 00:00:00 2001 From: Fabre Florian Date: Tue, 28 Jan 2025 16:08:35 +0100 Subject: [PATCH 19/29] Deprecate creme.ajax.URL. Remove dependency to jQuery and use native URLSearchParams. --- .eslintrc | 4 +- CHANGELOG.txt | 6 + .../activities/js/activities-calendar.js | 6 +- .../creme_core/js/lib/underscore/object.js | 17 +- .../static/creme_core/js/lib/url.js | 207 ++++++++++++++++++ .../static/creme_core/js/list_view.core.js | 2 +- .../creme_core/static/creme_core/js/search.js | 6 +- .../static/creme_core/js/tests/ajax/utils.js | 39 ++++ .../js/tests/list/listview-actions.js | 2 +- .../creme_core/js/tests/list/listview-core.js | 4 +- .../creme_core/js/tests/underscore/object.js | 16 ++ .../static/creme_core/js/tests/url.js | 38 ++++ .../creme_core/static/creme_core/js/utils.js | 6 +- .../creme_core/js/widgets/ajax/mockbackend.js | 2 +- .../static/creme_core/js/widgets/ajax/url.js | 165 ++------------ .../js/widgets/component/action-registry.js | 4 +- .../static/creme_core/js/widgets/editor.js | 6 +- .../static/emails/js/tests/emails-actions.js | 2 +- .../geolocation/js/geolocation-google.js | 4 +- .../reports/js/tests/reports-actions.js | 8 +- .../reports/js/tests/reports-listview.js | 8 +- creme/settings.py | 1 + 22 files changed, 371 insertions(+), 182 deletions(-) create mode 100644 creme/creme_core/static/creme_core/js/lib/url.js create mode 100644 creme/creme_core/static/creme_core/js/tests/url.js diff --git a/.eslintrc b/.eslintrc index 1b2fb20518..2d9dabfa0b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -28,7 +28,9 @@ "Set": true, "ResizeObserver": true, "FormData": true, - + "URL": true, + "URLSearchParams": true, + /* External libraries */ "jQuery": true, "$": true, diff --git a/CHANGELOG.txt b/CHANGELOG.txt index adf5095c0d..00cfd4c333 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -184,6 +184,11 @@ # Javascript: * Deprecations: - Array.copy(iterable, start, end) is deprecated; Use native code Array.from(iterable).slice(start, end) instead. + - creme.ajax.URL is deprecated; use RelativeURL instead + - creme.ajax.parseUrl() is deprecated; Use _.urlAsDict() instead + - creme.ajax.params() is deprecated; Use _.encodeURLSearch() instead + - creme.ajax.decodeSearchData() is deprecated; Use _.decodeURLSearchData() instead + - Add RelativeURL() class as an alternative of URL() that do not accepts relative paths (e.g '/list?page=1' is not a valid URL) * Use Object.assign instead of $.extend to remove the dependency to jQuery for : RGBColor, DateFaker, BrowserVersion & Assert. * Use Object.assign instead of $.extend for sketch components. * FormDialog: @@ -253,6 +258,7 @@ - Add support for download & upload progress event : new 'uploadProgress' & 'progress' options. - Refactor 'ListViewController' & 'creme.bricks.BricksReloader' - 'creme.ajax.Query' now accepts new 'uploadProgress' & 'progress' options. + * creme.ajax.URL.prototype.relativeTo() is replaced by RelativeURL.prototype.fullPath() - Apps : * Activities: - Refactor creme.ActivityCalendar : can be now used as independent component. diff --git a/creme/activities/static/activities/js/activities-calendar.js b/creme/activities/static/activities/js/activities-calendar.js index c59d19d6de..1aee08f98c 100644 --- a/creme/activities/static/activities/js/activities-calendar.js +++ b/creme/activities/static/activities/js/activities-calendar.js @@ -155,8 +155,8 @@ creme.ActivityCalendarController = creme.component.Component.sub({ }, _loadStateFromUrl: function(url) { - var hash = creme.ajax.parseUrl(url || this._currentUrl()).hash || ''; - var data = creme.ajax.decodeSearchData(hash.slice(1)); + var hash = _.urlAsDict(url || this._currentUrl()).hash || ''; + var data = _.decodeURLSearchData(hash.slice(1)); var view = data.view && (this.fullCalendarViewNames().indexOf(data.view) !== -1 ? data.view : this.prop('defaultView')); var date = data.date ? moment(data.date) : undefined; date = date && date.isValid() ? date : undefined; @@ -165,7 +165,7 @@ creme.ActivityCalendarController = creme.component.Component.sub({ }, _storeStateInUrl: function(state) { - creme.history.push('#' + creme.ajax.param({ + creme.history.push('#' + _.encodeURLSearch({ view: state.view, date: state.date.format('YYYY-MM-DD') })); diff --git a/creme/creme_core/static/creme_core/js/lib/underscore/object.js b/creme/creme_core/static/creme_core/js/lib/underscore/object.js index 731a940317..2d34da7da1 100644 --- a/creme/creme_core/static/creme_core/js/lib/underscore/object.js +++ b/creme/creme_core/static/creme_core/js/lib/underscore/object.js @@ -29,8 +29,23 @@ function pop(object, name, defaults) { } } +function append(object, key, value) { + var entry = object[key]; + + if (entry === undefined) { + entry = value; + } else if (Array.isArray(entry)) { + entry.push(value); + } else { + entry = [entry, value]; + } + + object[key] = entry; +} + _.mixin({ - pop: pop + pop: pop, + append: append }); }()); diff --git a/creme/creme_core/static/creme_core/js/lib/url.js b/creme/creme_core/static/creme_core/js/lib/url.js new file mode 100644 index 0000000000..b54475a54a --- /dev/null +++ b/creme/creme_core/static/creme_core/js/lib/url.js @@ -0,0 +1,207 @@ +/******************************************************************************* + Creme is a free/open-source Customer Relationship Management software + Copyright (C) 2025 Hybird + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*******************************************************************************/ + +(function() { +"use strict"; + +function decodeURLSearchData(search) { + search = search.replace(/^\?/, ''); + + var searchData = {}; + + if (search) { + var query = search.split('&'); + + query.forEach(function(e) { + var item = e.split('='); + var key = decodeURIComponent(item[0]); + var value = decodeURIComponent(item[1]); + + _.append(searchData, key, value); + }); + } + + return searchData; +}; + +function decodeURLSearchParams(params) { + var data = {}; + + params.forEach(function(value, key) { + _.append(data, key, value); + }); + + return data; +} + +function toURLSearchParams(data) { + if (data instanceof URLSearchParams) { + return data; + } else if (_.isString(data)) { + return new URLSearchParams(data); + } + + var params = new URLSearchParams(); + + Object.entries(data || {}).forEach(function(e) { + var key = e[0], value = e[1]; + + if (Array.isArray(value)) { + if (value.length > 0) { + value.forEach(function(item) { + params.append(key, item); + }); + } + } else if (value !== null && value !== undefined) { + params.append(key, value); + } + }); + + return params; +} + +window.RelativeURL = function(url) { + this._link = document.createElement('a'); + this._link.href = (url instanceof URL) ? url.toString() : url; +}; + +window.RelativeURL.prototype = { + _property: function(name, value) { + if (value === undefined) { + return this._link[name]; + } + + this._link[name] = value; + return this; + }, + + href: function(href) { + return this._property('href', href); + }, + + fullPath: function() { + return this._link.pathname + this._link.search + this._link.hash; + }, + + username: function(username) { + return this._property('username', username); + }, + + password: function(password) { + return this._property('password', password); + }, + + protocol: function(protocol) { + return this._property('protocol', protocol); + }, + + host: function(host) { + return this._property('host', host); + }, + + hostname: function(hostname) { + return this._property('hostname', hostname); + }, + + port: function(port) { + return this._property('port', port); + }, + + pathname: function(pathname) { + return this._property('pathname', pathname); + }, + + search: function(search) { + if (search === undefined) { + return this._link.search; + } + + search = (search instanceof URLSearchParams) ? search.toString() : search; + this._link.search = search; + return this; + }, + + hash: function(hash) { + return this._property('hash', hash); + }, + + properties: function() { + var url = this._link; + + return { + href: url.href, + username: url.username, + password: url.password, + protocol: url.protocol, + host: url.host, + hostname: url.hostname, + port: url.port, + pathname: url.pathname, + search: url.search, + searchData: decodeURLSearchData(url.search), + hash: url.hash + }; + }, + + searchParams: function(params) { + if (params === undefined) { + return new URLSearchParams(this._link.search); + } + + this._link.search = toURLSearchParams(params).toString(); + }, + + searchData: function(data) { + if (data === undefined) { + return decodeURLSearchData(this._link.search); + } + + this._link.search = toURLSearchParams(data).toString(); + return this; + }, + + updateSearchData: function(data) { + var entries = (data instanceof URLSearchParams) ? decodeURLSearchParams(data) : data; + var origin = this.searchData(); + var params = toURLSearchParams(Object.assign(origin, entries)); + + this._link.search = params.toString(); + return this; + }, + + toString: function() { + return this._link.toString(); + } +}; + +_.mixin({ + toRelativeURL: function(url) { + return new window.RelativeURL(url); + }, + urlAsDict: function(url) { + return new window.RelativeURL(url).properties(); + }, + toURLSearchParams: toURLSearchParams, + decodeURLSearchData: decodeURLSearchData, + decodeURLSearchParams: decodeURLSearchParams, + encodeURLSearch: function(data) { + return _.toURLSearchParams(data).toString(); + } +}); + +}()); diff --git a/creme/creme_core/static/creme_core/js/list_view.core.js b/creme/creme_core/static/creme_core/js/list_view.core.js index 43ba42d839..2e7634c200 100644 --- a/creme/creme_core/static/creme_core/js/list_view.core.js +++ b/creme/creme_core/static/creme_core/js/list_view.core.js @@ -487,7 +487,7 @@ }, nextStateUrl: function(data) { - var link = new creme.ajax.URL(this.reloadUrl()); + var link = _.toRelativeURL(this.reloadUrl()); // HACK : Since we don't have a specific view to reset the search // state, we must cleanup the urls to prevent unexpected "search=clear" diff --git a/creme/creme_core/static/creme_core/js/search.js b/creme/creme_core/static/creme_core/js/search.js index 5d4f9d2f31..f7171e73ad 100644 --- a/creme/creme_core/static/creme_core/js/search.js +++ b/creme/creme_core/static/creme_core/js/search.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2015-2024 Hybird + Copyright (C) 2015-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -164,7 +164,7 @@ creme.search.SearchBox = creme.component.Component.sub({ if (results.count > 0) { this._allResultsGroup.after(results.items); - var url = new creme.ajax.URL(this.advancedSearchUrl).searchData({search: results.query}); + var url = _.toRelativeURL(this.advancedSearchUrl).searchData({search: results.query}); this._allResultsLink.attr('href', url); this._allResultsLink.text(gettext('All results (%s)').format(results.count)); @@ -268,7 +268,7 @@ creme.search.SearchBox = creme.component.Component.sub({ results.push(bestResult); } - var searchUrl = new creme.ajax.URL(this.advancedSearchUrl); + var searchUrl = _.toRelativeURL(this.advancedSearchUrl); // CTs for (idx in data.results) { diff --git a/creme/creme_core/static/creme_core/js/tests/ajax/utils.js b/creme/creme_core/static/creme_core/js/tests/ajax/utils.js index 530709f28d..4fae3f4623 100644 --- a/creme/creme_core/static/creme_core/js/tests/ajax/utils.js +++ b/creme/creme_core/static/creme_core/js/tests/ajax/utils.js @@ -175,6 +175,12 @@ QUnit.test('creme.ajax.URL (property)', function(assert) { url.hash('#hackish'); equal('https://other:password@yetanother.admin.com:8090/this/is/another/test?a=1&a=2&b=true&c=a&d=&d=#hackish', url.href()); + + url.search('a=8&b=false&d='); + equal('https://other:password@yetanother.admin.com:8090/this/is/another/test?a=8&b=false&d=#hackish', url.href()); + + url.search(new URLSearchParams({x: 8, y: -5})); + equal('https://other:password@yetanother.admin.com:8090/this/is/another/test?x=8&y=-5#hackish', url.href()); }); QUnit.test('creme.ajax.URL (searchData)', function(assert) { @@ -215,6 +221,36 @@ QUnit.test('creme.ajax.URL (searchData, setter)', function(assert) { equal('http://admin.com:8080/this/is/a/test?a%5Bone%5D=1&b=b%3D1%2C2%2C3&c%5B%5D=1&c%5B%5D=2&c%5B%5D=3', url.href()); equal('?a%5Bone%5D=1&b=b%3D1%2C2%2C3&c%5B%5D=1&c%5B%5D=2&c%5B%5D=3', url.search()); deepEqual({'a[one]': '1', b: 'b=1,2,3', 'c[]': ['1', '2', '3']}, url.searchData()); + + url.searchData(new URLSearchParams({x: 8, y: -5, z: 30, a: 'b'})); + + equal('http://admin.com:8080/this/is/a/test?x=8&y=-5&z=30&a=b', url.href()); + equal('?x=8&y=-5&z=30&a=b', url.search()); + deepEqual({x: '8', y: '-5', z: '30', a: 'b'}, url.searchData()); + + url.searchData('a=4&c=8'); + deepEqual({a: '4', c: '8'}, url.searchData()); +}); + +QUnit.test('creme.ajax.URL (searchParams)', function(assert) { + var url = new creme.ajax.URL('http://admin.com:8080/this/is/a/test?a=1&b=2&c=true&d='); + + equal(new URLSearchParams({ + a: '1', + b: '2', + c: 'true', + d: '' + }).toString(), url.searchParams().toString()); + + url.searchParams(new URLSearchParams({x: 8, y: -5, z: 30, a: 'b'})); + + equal('http://admin.com:8080/this/is/a/test?x=8&y=-5&z=30&a=b', url.href()); + equal('?x=8&y=-5&z=30&a=b', url.search()); + deepEqual({x: '8', y: '-5', z: '30', a: 'b'}, url.searchData()); + + url.searchParams({x: 1, y: -1, z: 0}); + + equal('http://admin.com:8080/this/is/a/test?x=1&y=-1&z=0', url.href()); }); QUnit.test('creme.ajax.URL (updateSearchData)', function(assert) { @@ -237,6 +273,9 @@ QUnit.test('creme.ajax.URL (updateSearchData)', function(assert) { e: ['a', 'b'] }, url.searchData()); equal('http://admin.com:8080/this/is/a/test?a=1&b=5&c=true&d=&e=a&e=b', url.href()); + + url.updateSearchData(new URLSearchParams({x: 8, y: -5, a: '33'})); + equal('http://admin.com:8080/this/is/a/test?a=33&b=5&c=true&d=&e=a&e=b&x=8&y=-5', url.href()); }); QUnit.test('creme.ajax.cookieAttr', function(assert) { diff --git a/creme/creme_core/static/creme_core/js/tests/list/listview-actions.js b/creme/creme_core/static/creme_core/js/tests/list/listview-actions.js index a0ab773c81..dff8b26570 100644 --- a/creme/creme_core/static/creme_core/js/tests/list/listview-actions.js +++ b/creme/creme_core/static/creme_core/js/tests/list/listview-actions.js @@ -938,7 +938,7 @@ QUnit.test('creme.listview.row-action (redirect)', function(assert) { this.assertClosedPopover(); deepEqual([ - (new creme.ajax.URL(window.location.href).relativeUrl() + '?redirect#hatbar') + (_.toRelativeURL(window.location.href).fullPath() + '?redirect#hatbar') ], this.mockRedirectCalls()); }); diff --git a/creme/creme_core/static/creme_core/js/tests/list/listview-core.js b/creme/creme_core/static/creme_core/js/tests/list/listview-core.js index 5742ac535a..a02f54648b 100644 --- a/creme/creme_core/static/creme_core/js/tests/list/listview-core.js +++ b/creme/creme_core/static/creme_core/js/tests/list/listview-core.js @@ -525,7 +525,7 @@ QUnit.test('creme.listview.core (submitState)', function(assert) { ['done', html] ], this.mockListenerCalls('submit-complete')); - var nextUrl = new creme.ajax.URL('mock/listview/reload').updateSearchData({ + var nextUrl = _.toRelativeURL('mock/listview/reload').updateSearchData({ sort_key: ['regular_field-name'], sort_order: ['ASC'], selection: ['multiple'], @@ -1029,7 +1029,7 @@ QUnit.test('creme.listview.core (resetSearchState)', function(assert) { ['done', html] ], this.mockListenerCalls('submit-complete')); - var nextUrl = new creme.ajax.URL('mock/listview/reload').updateSearchData({ + var nextUrl = _.toRelativeURL('mock/listview/reload').updateSearchData({ sort_key: ['regular_field-name'], sort_order: ['ASC'], selection: ['multiple'], diff --git a/creme/creme_core/static/creme_core/js/tests/underscore/object.js b/creme/creme_core/static/creme_core/js/tests/underscore/object.js index cccba66632..97b25098be 100644 --- a/creme/creme_core/static/creme_core/js/tests/underscore/object.js +++ b/creme/creme_core/static/creme_core/js/tests/underscore/object.js @@ -22,4 +22,20 @@ QUnit.test('pop', function(assert) { deepEqual(data, {}); }); +QUnit.test('append', function(assert) { + var data = {}; + + _.append(data, 'a', 1); + deepEqual(data, {a: 1}); + + _.append(data, 'a', 2); + deepEqual(data, {a: [1, 2]}); + + _.append(data, 'a', [3, 4]); + deepEqual(data, {a: [1, 2, [3, 4]]}); + + _.append(data, 'b', 'hello'); + deepEqual(data, {a: [1, 2, [3, 4]], b: 'hello'}); +}); + }()); diff --git a/creme/creme_core/static/creme_core/js/tests/url.js b/creme/creme_core/static/creme_core/js/tests/url.js new file mode 100644 index 0000000000..3f5a713e72 --- /dev/null +++ b/creme/creme_core/static/creme_core/js/tests/url.js @@ -0,0 +1,38 @@ +/* globals RelativeURL */ + +(function() { + +// TODO : move URL tests from ajax/utils.js here. + +QUnit.module("RelativeURL", new QUnitMixin()); + +QUnit.test('RelativeURL (URL)', function(assert) { + var url = new RelativeURL(new URL('http://joe:pwd@admin.com:8080/this/is/a/test?a=1&a=2&b=true&c=a&d=&d=#hash')); + + deepEqual({ + href: 'http://joe:pwd@admin.com:8080/this/is/a/test?a=1&a=2&b=true&c=a&d=&d=#hash', + protocol: 'http:', + username: 'joe', + password: 'pwd', + host: 'admin.com:8080', + hostname: 'admin.com', + port: '8080', + pathname: '/this/is/a/test', + search: '?a=1&a=2&b=true&c=a&d=&d=', + searchData: {a: ['1', '2'], b: 'true', c: 'a', d: ['', '']}, + hash: '#hash' + }, url.properties()); +}); + +QUnit.test('RelativeURL.fullPath', function(assert) { + var url = new RelativeURL('http://joe:pwd@admin.com:8080/this/is/a/test?a=1&a=2&b=true&c=a&d=&d=#hash'); + equal(url.fullPath(), '/this/is/a/test?a=1&a=2&b=true&c=a&d=&d=#hash'); + + url = new RelativeURL('/this/is/a/test?a=1&a=2&b=true&c=a&d=&d=#hash'); + equal(url.fullPath(), '/this/is/a/test?a=1&a=2&b=true&c=a&d=&d=#hash'); + + url.searchData({x: 1, y: 2}); + equal(url.fullPath(), '/this/is/a/test?x=1&y=2#hash'); +}); + +}()); diff --git a/creme/creme_core/static/creme_core/js/utils.js b/creme/creme_core/static/creme_core/js/utils.js index 40f501e222..38d8c7411e 100644 --- a/creme/creme_core/static/creme_core/js/utils.js +++ b/creme/creme_core/static/creme_core/js/utils.js @@ -37,17 +37,17 @@ creme.utils.redirect = function(url) { creme.utils.locationRelativeUrl = function() { // remove 'http://host.com' - return (new creme.ajax.URL(window.location.href)).relativeUrl(); + return _.toRelativeURL(window.location.href).fullPath(); }; creme.utils.goTo = function(url, data) { if (Object.isEmpty(data)) { creme.utils.redirect(url); } else { - var urlinfo = new creme.ajax.URL(url); + var urlinfo = _.toRelativeURL(url); if (Object.isString(data)) { - data = creme.ajax.decodeSearchData(data); + data = _.decodeURLSearchData(data); } urlinfo.searchData($.extend({}, urlinfo.searchData(), data)); diff --git a/creme/creme_core/static/creme_core/js/widgets/ajax/mockbackend.js b/creme/creme_core/static/creme_core/js/widgets/ajax/mockbackend.js index 68aaedfdd2..d4b9252d7d 100644 --- a/creme/creme_core/static/creme_core/js/widgets/ajax/mockbackend.js +++ b/creme/creme_core/static/creme_core/js/widgets/ajax/mockbackend.js @@ -59,7 +59,7 @@ $.extend(creme.ajax.MockAjaxBackend.prototype, { } if (options.enableUriSearch) { - var urlInfo = new creme.ajax.URL(url); + var urlInfo = _.toRelativeURL(url); var searchData = urlInfo.searchData(); if (Object.isEmpty(searchData) === false) { diff --git a/creme/creme_core/static/creme_core/js/widgets/ajax/url.js b/creme/creme_core/static/creme_core/js/widgets/ajax/url.js index ca203825ec..95e3c04e29 100644 --- a/creme/creme_core/static/creme_core/js/widgets/ajax/url.js +++ b/creme/creme_core/static/creme_core/js/widgets/ajax/url.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2018-2024 Hybird + Copyright (C) 2018-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -15,167 +15,32 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . *******************************************************************************/ +/* globals RelativeURL */ -(function($) { +(function() { "use strict"; creme.ajax = creme.ajax || {}; -var __decodeSearchData = creme.ajax.decodeSearchData = function(search) { - search = search.replace(/^\?/, ''); - - var searchData = {}; - - if (search) { - var query = search.split('&'); - - query.forEach(function(e) { - var item = e.split('='); - var key = decodeURIComponent(item[0]); - var value = decodeURIComponent(item[1]); - var entry = searchData[key]; - - if (entry === undefined) { - entry = value; - } else if (Array.isArray(entry)) { - entry.push(value); - } else { - entry = [entry, value]; - } - - searchData[key] = entry; - }); - } - - return searchData; -}; - -creme.ajax.URL = creme.component.Component.sub({ - _init_: function(url) { - this._parser = document.createElement('a'); - this._parser.href = url; - this._searchData = __decodeSearchData(this._parser.search); - }, - - _property: function(name, value) { - if (value === undefined) { - return this._parser[name]; - } - - this._parser[name] = value; - return this; - }, - - href: function(href) { - return this._property('href', href); - }, - - relativeUrl: function() { - return '${pathname}${search}${hash}'.template(this._parser); - }, - - username: function(username) { - return this._property('username', username); - }, - - password: function(password) { - return this._property('password', password); - }, - - protocol: function(protocol) { - return this._property('protocol', protocol); - }, - - host: function(host) { - return this._property('host', host); - }, - - hostname: function(hostname) { - return this._property('hostname', hostname); - }, - - port: function(port) { - return this._property('port', port); - }, - - pathname: function(pathname) { - return this._property('pathname', pathname); - }, - - search: function(search) { - if (search === undefined) { - return this._parser.search; - } - - this._parser.search = search; - this._searchData = __decodeSearchData(search); - return this; - }, - - hash: function(hash) { - return this._property('hash', hash); - }, - - properties: function() { - return { - href: this.href(), - username: this.username(), - password: this.password(), - protocol: this.protocol(), - host: this.host(), - hostname: this.hostname(), - port: this.port(), - pathname: this.pathname(), - search: this.search(), - searchData: this.searchData(), - hash: this.hash() - }; - }, - - searchData: function(data) { - if (data === undefined) { - return this._searchData; - } - - this.search(creme.ajax.param(data)); - return this; - }, - - updateSearchData: function(data) { - data = data || {}; - return this.searchData($.extend({}, this._searchData, data)); - }, - - toString: function() { - return this._parser.toString(); - } -}); +creme.ajax.URL = RelativeURL; creme.ajax.parseUrl = function(url) { - var parser = document.createElement('a'); - - parser.href = url; - - return { - href: parser.href, - username: parser.username, - password: parser.password, - protocol: parser.protocol, - host: parser.host, - hostname: parser.hostname, - port: parser.port, - pathname: parser.pathname, - search: parser.search, - searchData: __decodeSearchData(parser.search), - hash: parser.hash - }; + console.warn('creme.ajax.parseUrl() is deprecated; Use _.urlAsDict() instead'); + return _.urlAsDict(url); }; creme.ajax.param = function(data) { // Use explicit traditional=true argument to replace ajaxSettings.traditional deprecated // since jQuery 1.9 see (https://bugs.jquery.com/ticket/12137) // return $.param(data, jQuery.ajaxSettings.traditional); - return $.param(data, true); + // return $.param(data, true); + console.warn('creme.ajax.params() is deprecated; Use _.encodeURLSearch() instead'); + return _.encodeURLSearch(data); +}; + +creme.ajax.decodeSearchData = function(search) { + console.warn('creme.ajax.decodeSearchData() is deprecated; Use _.decodeURLSearchData() instead'); + return _.decodeURLSearchData(search); }; -}(jQuery)); +}()); diff --git a/creme/creme_core/static/creme_core/js/widgets/component/action-registry.js b/creme/creme_core/static/creme_core/js/widgets/component/action-registry.js index 3c47f9314e..09857965c9 100644 --- a/creme/creme_core/static/creme_core/js/widgets/component/action-registry.js +++ b/creme/creme_core/static/creme_core/js/widgets/component/action-registry.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2018-2024 Hybird + Copyright (C) 2018-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -35,7 +35,7 @@ creme.action.DefaultActionBuilderRegistry = creme.component.FactoryRegistry.sub( // comeback is a shortcut for ?callback_url=${location} if (options.comeback) { - resolvedUrl = (new creme.ajax.URL(resolvedUrl)).updateSearchData({ + resolvedUrl = _.toRelativeURL(resolvedUrl).updateSearchData({ callback_url: locationUrl }).toString(); } diff --git a/creme/creme_core/static/creme_core/js/widgets/editor.js b/creme/creme_core/static/creme_core/js/widgets/editor.js index 6b68c48b92..21fc44833c 100644 --- a/creme/creme_core/static/creme_core/js/widgets/editor.js +++ b/creme/creme_core/static/creme_core/js/widgets/editor.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2020-2023 Hybird + Copyright (C) 2020-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -24,10 +24,10 @@ function setUpTinymce(url) { var base; if (!url.match(/^(http|https):\/\//)) { - base = '${protocol}//${host}/'.template(creme.ajax.parseUrl(window.location.href)); + base = '${protocol}//${host}/'.template(_.urlAsDict(window.location.href)); url = base + url; } else { - base = '${protocol}//${host}/'.template(creme.ajax.parseUrl(url)); + base = '${protocol}//${host}/'.template(_.urlAsDict(url)); } tinymce.documentBaseURL = url; diff --git a/creme/emails/static/emails/js/tests/emails-actions.js b/creme/emails/static/emails/js/tests/emails-actions.js index 651f8fd855..6b3a39d60b 100644 --- a/creme/emails/static/emails/js/tests/emails-actions.js +++ b/creme/emails/static/emails/js/tests/emails-actions.js @@ -18,7 +18,7 @@ QUnit.module("creme.emails.brick.actions", new QUnitMixin(QUnitEventMixin, + '' + '').template({ url: url, - params: creme.ajax.param(data) + params: _.encodeURLSearch(data) }); return backend.response(200, html); diff --git a/creme/geolocation/static/geolocation/js/geolocation-google.js b/creme/geolocation/static/geolocation/js/geolocation-google.js index 983a88c2ae..e335008ad8 100644 --- a/creme/geolocation/static/geolocation/js/geolocation-google.js +++ b/creme/geolocation/static/geolocation/js/geolocation-google.js @@ -1,6 +1,6 @@ /******************************************************************************* Creme is a free/open-source Customer Relationship Management software - Copyright (C) 2020-2021 Hybird + Copyright (C) 2020-2025 Hybird This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -90,7 +90,7 @@ var GoogleAPILoader = creme.component.Action.sub({ }; script.type = 'text/javascript'; - script.src = 'https://maps.googleapis.com/maps/api/js?' + creme.ajax.param(parameters); + script.src = _.toRelativeURL('https://maps.googleapis.com/maps/api/js').searchData(parameters).toString(); document.head.appendChild(script); } diff --git a/creme/reports/static/reports/js/tests/reports-actions.js b/creme/reports/static/reports/js/tests/reports-actions.js index a9139e0c22..8160593098 100644 --- a/creme/reports/static/reports/js/tests/reports-actions.js +++ b/creme/reports/static/reports/js/tests/reports-actions.js @@ -72,7 +72,7 @@ QUnit.module("creme.reports.actions", new QUnitMixin(QUnitEventMixin, this.setMockBackendPOST({ 'mock/reports/filterform': function(url, data, options) { - var redirectUrl = 'mock/reports/download?' + creme.ajax.param(data); + var redirectUrl = 'mock/reports/download?' + _.encodeURLSearch(data); return backend.response(200, redirectUrl, {'content-type': 'text/plain'}); }, 'mock/reports/filterform/invalid': backend.response(200, this.createExportDialogHtml()), @@ -122,7 +122,7 @@ QUnit.test('creme.reports.hatbar.actions (reports-export, ok)', function(assert) this.assertClosedDialog(); - var downloadUrl = 'mock/reports/download?' + creme.ajax.param({ + var downloadUrl = 'mock/reports/download?' + _.encodeURLSearch({ doc_type: 'csv', date_field: '', date_filter_0: '', @@ -206,7 +206,7 @@ QUnit.test('creme.reports.PreviewController (preview or download)', function(ass element.find('[name="doc_type"]').val('csv'); element.find('[data-daterange-type]').val('previous_year').trigger('change'); - var downloadUrl = '/mock/reports/download?' + creme.ajax.param({ + var downloadUrl = '/mock/reports/download?' + _.encodeURLSearch({ doc_type: 'csv', date_field: '', date_filter_0: 'previous_year', @@ -214,7 +214,7 @@ QUnit.test('creme.reports.PreviewController (preview or download)', function(ass date_filter_2: '' }); - var previewUrl = '/mock/reports/preview?' + creme.ajax.param({ + var previewUrl = '/mock/reports/preview?' + _.encodeURLSearch({ doc_type: 'csv', date_field: '', date_filter_0: 'previous_year', diff --git a/creme/reports/static/reports/js/tests/reports-listview.js b/creme/reports/static/reports/js/tests/reports-listview.js index f59987ec05..d848691d9e 100644 --- a/creme/reports/static/reports/js/tests/reports-listview.js +++ b/creme/reports/static/reports/js/tests/reports-listview.js @@ -46,7 +46,7 @@ QUnit.module("creme.reports.listview.actions", new QUnitMixin(QUnitEventMixin, this.setMockBackendPOST({ 'mock/reports/filterform': function(url, data, options) { - var redirectUrl = 'mock/reports/download?' + creme.ajax.param(data); + var redirectUrl = 'mock/reports/download?' + _.encodeURLSearch(data); return backend.response(200, redirectUrl, {'content-type': 'text/plain'}); }, 'mock/reports/filterform/invalid': backend.response(200, MOCK_FILTERFORM_CONTENT), @@ -119,7 +119,7 @@ QUnit.test('creme.reports.ExportReportAction (csv, none)', function(assert) { this.assertClosedDialog(); - var download_url = 'mock/reports/download?' + creme.ajax.param({doc_type: 'csv', date_field: '', date_filter_0: '', date_filter_2: ''}); + var download_url = 'mock/reports/download?' + _.encodeURLSearch({doc_type: 'csv', date_field: '', date_filter_0: '', date_filter_2: ''}); deepEqual([['done', download_url]], this.mockListenerCalls('action-done')); @@ -158,7 +158,7 @@ QUnit.test('creme.reports.ExportReportAction (xls, created, previous_year)', fun this.assertClosedDialog(); - var download_url = 'mock/reports/download?' + creme.ajax.param({doc_type: 'xls', date_field: 'created', date_filter_0: 'previous_year', date_filter_2: ''}); + var download_url = 'mock/reports/download?' + _.encodeURLSearch({doc_type: 'xls', date_field: 'created', date_filter_0: 'previous_year', date_filter_2: ''}); deepEqual([['done', download_url]], this.mockListenerCalls('action-done')); @@ -201,7 +201,7 @@ QUnit.test('creme.reports.listview.actions (reports-export, ok)', function(asser this.assertClosedDialog(); - var download_url = 'mock/reports/download?' + creme.ajax.param({ + var download_url = 'mock/reports/download?' + _.encodeURLSearch({ doc_type: 'csv', date_field: '', date_filter_0: '', date_filter_2: '' }); diff --git a/creme/settings.py b/creme/settings.py index df78309d2d..b43db53033 100644 --- a/creme/settings.py +++ b/creme/settings.py @@ -871,6 +871,7 @@ 'creme_core/js/lib/assert.js', 'creme_core/js/lib/faker.js', 'creme_core/js/lib/browser.js', + 'creme_core/js/lib/url.js', # Legacy tools 'creme_core/js/creme.js', From e7f613cdfe59823dbaffa44331ed272ee24680d4 Mon Sep 17 00:00:00 2001 From: joehybird Date: Wed, 12 Feb 2025 08:14:46 +0100 Subject: [PATCH 20/29] Improve D3ChartBrickController coverage. --- creme/sketch/static/sketch/js/tests/bricks.js | 81 ++++++++++++++++++- .../sketch/js/tests/qunit-sketch-mixin.js | 7 +- 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/creme/sketch/static/sketch/js/tests/bricks.js b/creme/sketch/static/sketch/js/tests/bricks.js index 483aac8b7b..92d9347e3c 100644 --- a/creme/sketch/static/sketch/js/tests/bricks.js +++ b/creme/sketch/static/sketch/js/tests/bricks.js @@ -178,6 +178,30 @@ QUnit.test('creme.D3ChartBrickPopoverAction', function(assert) { deepEqual([['done']], this.mockListenerCalls('action-done')); }); + + this.resetMockListenerCalls('action-done'); + + this.withFakeMethod({ + instance: chart, + method: 'asImage', + callable: function(done) { + done(); + } + }, function(faker) { + var options = {width: 150, height: 200}; + + deepEqual([], this.mockListenerCalls('action-done'), ''); + + action.start(options); + + equal(faker.count(), 1, 'asImage called once'); + deepEqual(faker.calls()[0].slice(1), [{width: 150, height: 200}]); + + // No image, No popover + this.assertClosedPopover(); + + deepEqual([['done']], this.mockListenerCalls('action-done')); + }); }); QUnit.parametrize('creme.setupD3ChartBrick', [ @@ -189,7 +213,11 @@ QUnit.parametrize('creme.setupD3ChartBrick', [ var chart = new FakeD3Chart(); var html = this.createD3ChartBrickHtml({ data: data, - props: props + props: props, + header: ( + '' + + '' + ) }); var element = $(html).appendTo(this.qunitFixture()); @@ -211,4 +239,55 @@ QUnit.parametrize('creme.setupD3ChartBrick', [ equal(true, brick.getActionBuilders().builders().indexOf('sketch-popover') !== -1); }); +QUnit.test('creme.setupD3ChartBrick (links)', function(assert) { + var chart = new FakeD3Chart(); + var html = this.createD3ChartBrickHtml({ + header: ( + '' + + '' + ) + }); + var element = $(html).appendTo(this.qunitFixture()); + + creme.setupD3ChartBrick(element, {chart: chart}); + var brick = creme.widget.create(element).brick(); + + // actions are registered + equal(true, brick.getActionBuilders().builders().indexOf('sketch-download') !== -1); + equal(true, brick.getActionBuilders().builders().indexOf('sketch-popover') !== -1); + + // try sketch-download + this.withFakeMethod({ + instance: chart, + method: 'saveAs', + callable: function(done, filename, options) { + done(); + } + }, function(faker) { + var options = {filename: 'my-sketch.svg', width: 150, height: 200}; + + brick.element().find('.download').trigger('click'); + + equal(faker.count(), 1); + deepEqual(faker.calls()[0].slice(1), ['my-sketch.svg', options]); + }); + + // try sketch-popover + this.withFakeMethod({ + instance: chart, + method: 'asImage', + callable: function(done) { + done($('')); + } + }, function(faker) { + brick.element().find('.popover').trigger('click'); + + equal(faker.count(), 1, 'asImage called once'); + deepEqual(faker.calls()[0].slice(1), [{width: 150, height: 200}]); + + this.assertOpenedPopover(); + this.closePopover(); + }); +}); + }(jQuery, QUnit)); diff --git a/creme/sketch/static/sketch/js/tests/qunit-sketch-mixin.js b/creme/sketch/static/sketch/js/tests/qunit-sketch-mixin.js index 8422970ee5..2e2d35840a 100644 --- a/creme/sketch/static/sketch/js/tests/qunit-sketch-mixin.js +++ b/creme/sketch/static/sketch/js/tests/qunit-sketch-mixin.js @@ -42,17 +42,20 @@ window.QUnitSketchMixin = { createD3ChartBrickHtml: function(options) { options = $.extend({ data: [], - props: {} + props: {}, + header: '' }, options || {}); var content; if (!Object.isEmpty(options.data)) { content = ( + '
      ${header}
      ' + '
      ' + '' ).template({ - data: JSON.stringify(options.data) + data: JSON.stringify(options.data), + header: options.header }); } else { content = '
      '; From 7d759c0c9ca6777070614d86e878966258faeee8 Mon Sep 17 00:00:00 2001 From: Fabre Florian Date: Wed, 12 Feb 2025 11:35:27 +0100 Subject: [PATCH 21/29] Improve creme.widget.SelectorList coverage --- .../js/tests/widgets/selectorlist.js | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/creme/creme_core/static/creme_core/js/tests/widgets/selectorlist.js b/creme/creme_core/static/creme_core/js/tests/widgets/selectorlist.js index a6e624d2c7..e472672dc6 100644 --- a/creme/creme_core/static/creme_core/js/tests/widgets/selectorlist.js +++ b/creme/creme_core/static/creme_core/js/tests/widgets/selectorlist.js @@ -176,6 +176,29 @@ QUnit.test('creme.widgets.selectorlist.create (value, no selector)', function(as equal(widget.selectors().length, 0); }); +QUnit.test('creme.widgets.selectorlist.create (value, invalid selector)', function(assert) { + var element = this.createSelectorListTag(JSON.stringify([3])); + this.appendSelectorListModelTag(element, $('
      ')); + + var widget = creme.widget.create(element); + + equal(element.hasClass('widget-active'), true); + equal(element.hasClass('widget-ready'), true); + + equal(widget.val(), JSON.stringify([])); + equal(widget.selectorModel().length, 1); + equal(widget.lastSelector().length, 0); + equal(widget.selectors().length, 0); + + var last = widget.appendSelector(13); + equal(last, undefined); + + equal(widget.val(), JSON.stringify([])); + equal(widget.selectorModel().length, 1); + equal(widget.lastSelector().length, 0); + equal(widget.selectors().length, 0); +}); + QUnit.test('creme.widgets.selectorlist.create (value, static selector)', function(assert) { var element = this.createSelectorListTag(JSON.stringify([3, 5, 3, 15])); var ctype = this.createDynamicSelectTag(); @@ -805,5 +828,58 @@ QUnit.test('creme.widgets.selectorlist.appendLast (not empty, no clone last)', f equal(widget.selectorModel().length, 1); equal(widget.selectors().length, 3); }); + +QUnit.test('creme.widgets.selectorlist (add button)', function(assert) { + var element = this.createSelectorListTag(JSON.stringify([{ctype: '3', rtype: '1'}, {ctype: '5', rtype: '6'}])); + var model = this.createCTypeRTypeSelectorTag(); + + this.appendSelectorListModelTag(element, model); + element.append(' +
    • +
    + +
      +
    • +
    • +
    • h
    • +
    • +
    • +
    • +
    + +
      +
    • +
    • +
    • h
    • +
    • +
    • +
    • +
    + +
      +
    • +
    • +
    • h
    • +
    • +
    • +
    • +
    +

    +{% endblock %} From 75c1714b76bccdc4138646ceaf16dc97450aefc2 Mon Sep 17 00:00:00 2001 From: Fabre Florian Date: Wed, 19 Feb 2025 16:38:29 +0100 Subject: [PATCH 27/29] Deprecate overlay tools in creme.utils. --- CHANGELOG.txt | 3 ++ .../chantilly/creme_core/css/list_view.css | 33 +++++++++++++++++++ .../static/creme_core/js/list_view.core.js | 18 +++++++--- .../creme_core/static/creme_core/js/utils.js | 5 +++ .../icecream/creme_core/css/list_view.css | 24 ++++++++++++++ 5 files changed, 78 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d324172ce7..ba071709f4 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -190,6 +190,9 @@ - creme.ajax.decodeSearchData() is deprecated; Use _.decodeURLSearchData() instead - creme.exports.exportAs() is deprecated; Use creme.lv_widget.DownloadAction instead. - creme.utils.confirmSubmit() is deprecated; Use the new 'creme_core-hatmenubar-delete' or 'delete' actions instead. + - creme.utils.showPageLoadOverlay() is deprecated; Use creme.dialog.Overlay instead. + - creme.utils.hidePageLoadOverlay() is deprecated; Use creme.dialog.Overlay instead. + - creme.utils.overlay() is deprecated; Use creme.dialog.Overlay instead. - Add RelativeURL() class as an alternative of URL() that do not accepts relative paths (e.g '/list?page=1' is not a valid URL) * Use Object.assign instead of $.extend to remove the dependency to jQuery for : RGBColor, DateFaker, BrowserVersion & Assert. * Use Object.assign instead of $.extend for sketch components. diff --git a/creme/creme_core/static/chantilly/creme_core/css/list_view.css b/creme/creme_core/static/chantilly/creme_core/css/list_view.css index 18e577d155..c7e96709cd 100644 --- a/creme/creme_core/static/chantilly/creme_core/css/list_view.css +++ b/creme/creme_core/static/chantilly/creme_core/css/list_view.css @@ -86,6 +86,39 @@ float: right; } +.ui-creme-listview-standalone .list-controls { + margin-top: -10px; + margin-right: 8px; +} + +.ui-creme-listview-standalone.is-loading { + overflow: visible !important; +} + +.ui-creme-listview-standalone .floatThead-container ~ .ui-creme-overlay { + z-index: 99; +} + +.ui-creme-listview .lv-loading { + background-color: white; + opacity: .70; + filter: Alpha(Opacity=70); +} + +.ui-creme-listview .lv-loading .overlay-content { + display: flex; + align-items: center; + justify-content: center; +} + +.ui-creme-listview .lv-loading .overlay-content span { + padding-left: 10px; +} + +.ui-creme-listview-popup .list-controls { + margin-top: 5px; +} + /* Listview page - page header and controls - end */ /* Listview page header buttons */ diff --git a/creme/creme_core/static/creme_core/js/list_view.core.js b/creme/creme_core/static/creme_core/js/list_view.core.js index 2e7634c200..0c9a610204 100644 --- a/creme/creme_core/static/creme_core/js/list_view.core.js +++ b/creme/creme_core/static/creme_core/js/list_view.core.js @@ -371,6 +371,7 @@ }); this._element = null; + this._overlay = new creme.dialog.Overlay(); this._loading = false; this.selectionMode(options.selectionMode); @@ -513,18 +514,17 @@ _updateLoadingState: function(state) { if (state !== this.isLoading()) { - /* - * TODO : Toggle css class like bricks - * this._element.toggleClass('is-loading', state); - */ - this._loading = state; + this._element.toggleClass('is-loading', state); + this._overlay.update(state, '', state ? 100 : 0); + /* if (state) { creme.utils.showPageLoadOverlay(); } else { creme.utils.hidePageLoadOverlay(); } + */ } }, @@ -609,6 +609,7 @@ var element = this._element; this._unbindColumnFilters(element); + this._overlay.unbind(element); this._element = null; return this; @@ -621,6 +622,13 @@ this._element = element; this._selections.bind(element); + this._overlay.addClass('lv-loading') + .content($( + '

    ${label}

    '.template({ + src: creme_media_url('images/wait.gif'), + label: gettext('Loading…') + }) + )).bind(element); this._bindActions(element); diff --git a/creme/creme_core/static/creme_core/js/utils.js b/creme/creme_core/static/creme_core/js/utils.js index 1a75ac57d5..cbd7b5c6a1 100644 --- a/creme/creme_core/static/creme_core/js/utils.js +++ b/creme/creme_core/static/creme_core/js/utils.js @@ -58,17 +58,21 @@ creme.utils.goTo = function(url, data) { // TODO : deprecate it ? never used creme.utils.showPageLoadOverlay = function() { // console.log('show loading overlay'); + console.warn('creme.utils.showPageLoadOverlay is deprecated; Use creme.dialog.Overlay instead.'); creme.utils.loading('', false); }; // TODO : deprecate it ? never used creme.utils.hidePageLoadOverlay = function() { // console.log('hide loading overlay'); + console.warn('creme.utils.showPageLoadOverlay is deprecated; Use creme.dialog.Overlay instead.'); creme.utils.loading('', true); }; // TODO : deprecate it ? Only used in old creme.ajax.* methods (see ajax.js) creme.utils.loading = function(div_id, is_loaded, params) { + console.warn('creme.utils.loading is deprecated; Use creme.dialog.Overlay instead.'); + var overlay = creme.utils._overlay; if (overlay === undefined) { @@ -223,6 +227,7 @@ creme.utils.ajaxQuery = function(url, options, data) { } }); + /* TODO : remove this feature. */ if (options.waitingOverlay) { query.onStart(function() { creme.utils.showPageLoadOverlay(); diff --git a/creme/creme_core/static/icecream/creme_core/css/list_view.css b/creme/creme_core/static/icecream/creme_core/css/list_view.css index bbd0b8f61a..f2b3f09068 100644 --- a/creme/creme_core/static/icecream/creme_core/css/list_view.css +++ b/creme/creme_core/static/icecream/creme_core/css/list_view.css @@ -135,6 +135,30 @@ margin-right: 8px; } +.ui-creme-listview-standalone.is-loading { + overflow: visible !important; +} + +.ui-creme-listview-standalone .floatThead-container ~ .ui-creme-overlay { + z-index: 99; +} + +.ui-creme-listview .lv-loading { + background-color: white; + opacity: .70; + filter: Alpha(Opacity=70); +} + +.ui-creme-listview .lv-loading .overlay-content { + display: flex; + align-items: center; + justify-content: center; +} + +.ui-creme-listview .lv-loading .overlay-content span { + padding-left: 10px; +} + .ui-creme-listview-popup .list-controls { margin-top: 5px; } From 91e6a39b4ba38f694a7ed3657188f1e5d065178e Mon Sep 17 00:00:00 2001 From: Fabre Florian Date: Thu, 20 Feb 2025 14:10:29 +0100 Subject: [PATCH 28/29] Add 'progress' & 'upload-progress' event listeners for creme.ajax.Query. Can be used along progress/uploadProgress callbacks. --- .../creme_core/js/tests/ajax/mockajax.js | 1 - .../static/creme_core/js/tests/ajax/query.js | 107 ++++++++++++++++++ .../creme_core/js/widgets/ajax/mockbackend.js | 8 +- .../creme_core/js/widgets/ajax/query.js | 29 ++++- 4 files changed, 138 insertions(+), 7 deletions(-) diff --git a/creme/creme_core/static/creme_core/js/tests/ajax/mockajax.js b/creme/creme_core/static/creme_core/js/tests/ajax/mockajax.js index 1abca9007e..5425420140 100644 --- a/creme/creme_core/static/creme_core/js/tests/ajax/mockajax.js +++ b/creme/creme_core/static/creme_core/js/tests/ajax/mockajax.js @@ -243,7 +243,6 @@ QUnit.test('MockAjaxBackend.submit', function(assert) { equal(response.status, 500); }); - QUnit.test('MockAjaxBackend.post (custom)', function(assert) { var response = {}; this.backend.post('mock/custom', {}, function(responseText) { $.extend(response, {responseText: responseText}); }, diff --git a/creme/creme_core/static/creme_core/js/tests/ajax/query.js b/creme/creme_core/static/creme_core/js/tests/ajax/query.js index bc77fbb8d5..e3e89befaf 100644 --- a/creme/creme_core/static/creme_core/js/tests/ajax/query.js +++ b/creme/creme_core/static/creme_core/js/tests/ajax/query.js @@ -1,3 +1,5 @@ +/* globals FunctionFaker */ + (function($) { QUnit.module("creme.ajax.query.js", new QUnitMixin(QUnitAjaxMixin, QUnitEventMixin, { buildMockBackend: function() { @@ -476,6 +478,110 @@ QUnit.test('creme.ajax.Query.post (fail)', function(assert) { deepEqual(this.mockListenerCalls('error'), this.mockListenerCalls('complete')); }); +QUnit.test('creme.ajax.Query (progress, all handlers)', function(assert) { + this.backend.progressSteps = [0, 10, 30, 50, 100]; + + var progressCb = new FunctionFaker(); + var progressEventCb = new FunctionFaker(); + var uploadCb = new FunctionFaker(); + var uploadEventCb = new FunctionFaker(); + + var query = new creme.ajax.Query({}, this.backend); + query.onProgress(progressEventCb.wrap()); + query.onUploadProgress(uploadEventCb.wrap()); + + query.get({}, { + progress: progressCb.wrap(), + uploadProgress: uploadCb.wrap() + }); + + deepEqual(0, progressCb.count()); + deepEqual(0, uploadCb.count()); + deepEqual(0, progressEventCb.count()); + deepEqual(0, uploadEventCb.count()); + + query.url('mock/custom').get({}, { + progress: progressCb.wrap(), + uploadProgress: uploadCb.wrap() + }); + + function progressCall(args) { + return [args[0], args[1].loadedPercent]; + } + + deepEqual(5, progressCb.count()); + deepEqual(5, uploadCb.count()); + deepEqual([ + ['progress', 0], + ['progress', 10], + ['progress', 30], + ['progress', 50], + ['progress', 100] + ], progressEventCb.calls().map(progressCall)); + deepEqual([ + ['upload-progress', 0], + ['upload-progress', 10], + ['upload-progress', 30], + ['upload-progress', 50], + ['upload-progress', 100] + ], uploadEventCb.calls().map(progressCall)); +}); + +QUnit.test('creme.ajax.Query (progress, only cb)', function(assert) { + this.backend.progressSteps = [0, 10, 30, 50, 100]; + + var progressCb = new FunctionFaker(); + var uploadCb = new FunctionFaker(); + + var query = new creme.ajax.Query({}, this.backend); + + query.get({}, { + progress: progressCb.wrap(), + uploadProgress: uploadCb.wrap() + }); + + deepEqual(0, progressCb.count()); + deepEqual(0, uploadCb.count()); + + query.url('mock/custom').get({}, { + progress: progressCb.wrap(), + uploadProgress: uploadCb.wrap() + }); + + function progressCall(args) { + return args[0].loadedPercent; + } + + deepEqual([0, 10, 30, 50, 100], progressCb.calls().map(progressCall)); + deepEqual([0, 10, 30, 50, 100], uploadCb.calls().map(progressCall)); +}); + +QUnit.test('creme.ajax.Query (progress, only event cb)', function(assert) { + this.backend.progressSteps = [0, 10, 30, 50, 100]; + + var progressEventCb = new FunctionFaker(); + var uploadEventCb = new FunctionFaker(); + + var query = new creme.ajax.Query({}, this.backend); + + query.onProgress(progressEventCb.wrap()); + query.onUploadProgress(uploadEventCb.wrap()); + + query.get(); + + deepEqual(0, progressEventCb.count()); + deepEqual(0, uploadEventCb.count()); + + query.url('mock/custom').get(); + + function progressCall(args) { + return args[1].loadedPercent; + } + + deepEqual([0, 10, 30, 50, 100], progressEventCb.calls().map(progressCall)); + deepEqual([0, 10, 30, 50, 100], uploadEventCb.calls().map(progressCall)); +}); + QUnit.test('creme.ajax.query (get)', function(assert) { var query = creme.ajax.query('mock/custom', {}, {}, this.backend); query.onDone(this.mockListener('success')); @@ -568,4 +674,5 @@ QUnit.test('creme.ajax.query (empty url)', function(assert) { ], this.mockListenerCalls('error')); deepEqual(this.mockListenerCalls('error'), this.mockListenerCalls('complete')); }); + }(jQuery)); diff --git a/creme/creme_core/static/creme_core/js/widgets/ajax/mockbackend.js b/creme/creme_core/static/creme_core/js/widgets/ajax/mockbackend.js index d4b9252d7d..da5e1e7aa1 100644 --- a/creme/creme_core/static/creme_core/js/widgets/ajax/mockbackend.js +++ b/creme/creme_core/static/creme_core/js/widgets/ajax/mockbackend.js @@ -43,7 +43,7 @@ $.extend(creme.ajax.MockAjaxBackend.prototype, { send: function(url, data, method, on_success, on_error, options) { var self = this; var method_urls = this[method] || {}; - options = $.extend({}, this.options, options); + options = $.extend({}, this.options, options || {}); if (options.sync !== true) { options.sync = true; @@ -97,7 +97,7 @@ $.extend(creme.ajax.MockAjaxBackend.prototype, { var responseData = response.responseText; - if (options.updateProgress || options.progress) { + if (options.uploadProgress || options.progress) { (this.progressSteps || []).forEach(function(step) { var progressEvent = new ProgressEvent('progress', { total: 1000, @@ -107,8 +107,8 @@ $.extend(creme.ajax.MockAjaxBackend.prototype, { progressEvent.loadedPercent = step; - if (options.updateProgress) { - options.updateProgress(progressEvent); + if (options.uploadProgress) { + options.uploadProgress(progressEvent); } if (options.progress) { diff --git a/creme/creme_core/static/creme_core/js/widgets/ajax/query.js b/creme/creme_core/static/creme_core/js/widgets/ajax/query.js index ee12d9d2b7..2b57e2b0d3 100644 --- a/creme/creme_core/static/creme_core/js/widgets/ajax/query.js +++ b/creme/creme_core/static/creme_core/js/widgets/ajax/query.js @@ -94,17 +94,42 @@ creme.ajax.Query = creme.component.Action.sub({ return this; }, + onProgress: function(progress) { + return this.on('progress', progress); + }, + + onUploadProgress: function(progress) { + return this.on('upload-progress', progress); + }, + _send: function(options) { options = $.extend(true, {}, this.options(), options || {}); + var self = this; var data = $.extend({}, this.data() || {}, options.data || {}); // TODO : replace 'action' by 'method' var action = (options.action || 'get').toLowerCase(); var backendOptions = options.backend || {}; var url = this.url() || ''; - backendOptions.progress = _.pop(options, 'progress', backendOptions.progress); - backendOptions.uploadProgress = _.pop(options, 'uploadProgress', backendOptions.uploadProgress); + var progressCb = _.pop(options, 'progress', backendOptions.progress); + var uploadProgressCb = _.pop(options, 'uploadProgress', backendOptions.uploadProgress); + + // progress is not often used, so we only create a callback when it is needed. + if (Object.isFunc(progressCb) || this._events.listeners('progress').length > 0) { + backendOptions.progress = function(e) { + self.trigger('progress', e); + (progressCb || _.noop)(e); + }; + } + + // Same optimization here + if (Object.isFunc(uploadProgressCb) || this._events.listeners('upload-progress').length > 0) { + backendOptions.uploadProgress = function(e) { + self.trigger('upload-progress', e); + (uploadProgressCb || _.noop)(e); + }; + } try { if (Object.isNone(this._backend)) { From 874113f3fb3178b703a983ae7a5dff2fbd59e975 Mon Sep 17 00:00:00 2001 From: Fabre Florian Date: Fri, 21 Feb 2025 11:19:15 +0100 Subject: [PATCH 29/29] Drop old (and unused) 'creme.object' methods : uuid, deferred_start & deferred_cancel --- .../creme_core/js/tests/widgets/base.js | 4 +- .../static/creme_core/js/widgets/base.js | 45 ------------------- 2 files changed, 2 insertions(+), 47 deletions(-) diff --git a/creme/creme_core/static/creme_core/js/tests/widgets/base.js b/creme/creme_core/static/creme_core/js/tests/widgets/base.js index 1acc33faba..cad422043e 100644 --- a/creme/creme_core/static/creme_core/js/tests/widgets/base.js +++ b/creme/creme_core/static/creme_core/js/tests/widgets/base.js @@ -243,7 +243,7 @@ QUnit.test('creme.object.delegate', function(assert) { equal(12, creme.object.delegate(instance, 'val')); equal(7, creme.object.delegate(instance, 'add', 3, 4)); }); - +/* QUnit.test('creme.object.deferred (finished)', function(assert) { var element = $('
    '); var result = []; @@ -324,7 +324,7 @@ QUnit.test('creme.object.deferred (restarted)', function(assert) { start(); }, 700); }); - +*/ QUnit.test('creme.object.build_callback (invalid script)', function(assert) { QUnit.assert.raises(function() { creme.object.build_callback('...'); }); QUnit.assert.raises(function() { creme.object.build_callback('{', ['arg1', 'arg2']); }); diff --git a/creme/creme_core/static/creme_core/js/widgets/base.js b/creme/creme_core/static/creme_core/js/widgets/base.js index a2917f912f..bade6beafa 100644 --- a/creme/creme_core/static/creme_core/js/widgets/base.js +++ b/creme/creme_core/static/creme_core/js/widgets/base.js @@ -57,51 +57,6 @@ creme.object = { return creme.utils.lambda(script, parameters); }, - deferred_start: function(element, name, func, delay) { - var key = 'deferred__' + name; - var previous = element.data(key); - - if (previous !== undefined) { - previous.reject(); - } - - var deferred = $.Deferred(); - - $.when(deferred.promise()).then(function(status) { - element.removeData(key); - creme.object.invoke(func, element, status); - }, null, null); - - element.data(key, deferred); - - if (delay) { - window.setTimeout(function() { - deferred.resolve(); - }, delay); - } else { - deferred.resolve(); - } - - return deferred; - }, - - deferred_cancel: function(element, name) { - var key = 'deferred__' + name; - var previous = element.data(key); - - if (previous !== undefined) { - element.removeData(key); - previous.reject(); - } - - return previous; - }, - - uuid: function() { - console.warn('Deprecated and only used by jQPlot; Use _.uniqueId() instead.'); - return _.uniqueId('widget_'); - }, - isFalse: function(value) { return Object.isNone(value) || value === false; },