From 735e8542a0d4243dd1a2b2a3a751769fc8ad0578 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Wed, 23 Mar 2022 09:10:24 +0200 Subject: [PATCH 01/55] fix(loadFromJSON): clear canvas clear canvas right before reload instead of before enlivening, which may take time in case of heavy images/resources --- src/mixins/canvas_serialization.mixin.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mixins/canvas_serialization.mixin.js b/src/mixins/canvas_serialization.mixin.js index e82d665b090..64274347262 100644 --- a/src/mixins/canvas_serialization.mixin.js +++ b/src/mixins/canvas_serialization.mixin.js @@ -36,7 +36,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return fabric.util.enlivenObjects(serialized.objects || [], '', reviver) .then(function(enlived) { - _this.clear(); + return fabric.util.enlivenObjectEnlivables({ backgroundImage: serialized.backgroundImage, backgroundColor: serialized.background, @@ -45,6 +45,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati clipPath: serialized.clipPath, }) .then(function(enlivedMap) { + _this.clear(); _this.__setupCanvas(serialized, enlived, renderOnAddRemove); _this.set(enlivedMap); return _this; From cb13c0c1e6f2eab16a5465c901263373183d8802 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Wed, 23 Mar 2022 09:12:51 +0200 Subject: [PATCH 02/55] Update canvas_serialization.mixin.js --- src/mixins/canvas_serialization.mixin.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/mixins/canvas_serialization.mixin.js b/src/mixins/canvas_serialization.mixin.js index 64274347262..0b0033ce7a7 100644 --- a/src/mixins/canvas_serialization.mixin.js +++ b/src/mixins/canvas_serialization.mixin.js @@ -36,7 +36,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return fabric.util.enlivenObjects(serialized.objects || [], '', reviver) .then(function(enlived) { - return fabric.util.enlivenObjectEnlivables({ backgroundImage: serialized.backgroundImage, backgroundColor: serialized.background, From d927c7eed77b53c1c01097e89d2ea6fb2b4d3915 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Wed, 23 Mar 2022 14:00:33 +0200 Subject: [PATCH 03/55] feat(util): support aborting `loadImage` --- src/util/misc.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/util/misc.js b/src/util/misc.js index a1310df4ad2..30e6397bc0c 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -525,13 +525,26 @@ * @param {String} url URL representing an image * @param {Object} [options] image loading options * @param {string} [options.crossOrigin] cors value for the image loading, default to anonymous + * @param {AbortSignal} [options.signal] see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal * @param {Promise} img the loaded image. */ - loadImage: function(url, options) { - return new Promise(function(resolve, reject) { + loadImage: function (url, options) { + var signal = options && options.signal; + var abort; + return new Promise(function (resolve, reject) { + if (signal && signal.aborted) { + return reject(new DOMException('`signal` is in `aborted` state', 'ABORT_ERR')); + } + else if (signal) { + abort = function () { + reject(new DOMException('aborted by user', 'ABORT_ERR')); + } + signal.addEventListener('abort', abort); + } var img = fabric.util.createImage(); var done = function() { img.onload = img.onerror = null; + signal && abort && signal.removeEventListener('abort', abort); resolve(img); }; if (!url) { From 47e98f49db68019359a57d5dafa941dcb99ccfd2 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Wed, 23 Mar 2022 14:03:49 +0200 Subject: [PATCH 04/55] refactor `renderOnAddRemove` --- src/mixins/canvas_serialization.mixin.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/mixins/canvas_serialization.mixin.js b/src/mixins/canvas_serialization.mixin.js index 0b0033ce7a7..ce7bcbef0f3 100644 --- a/src/mixins/canvas_serialization.mixin.js +++ b/src/mixins/canvas_serialization.mixin.js @@ -45,7 +45,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }) .then(function(enlivedMap) { _this.clear(); - _this.__setupCanvas(serialized, enlived, renderOnAddRemove); + _this.__setupCanvas(serialized, enlived); + _this.renderOnAddRemove = renderOnAddRemove; _this.set(enlivedMap); return _this; }); @@ -56,16 +57,14 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @private * @param {Object} serialized Object with background and overlay information * @param {Array} enlivenedObjects canvas objects - * @param {boolean} renderOnAddRemove renderOnAddRemove setting for the canvas */ - __setupCanvas: function(serialized, enlivenedObjects, renderOnAddRemove) { + __setupCanvas: function(serialized, enlivenedObjects) { var _this = this; enlivenedObjects.forEach(function(obj, index) { // we splice the array just in case some custom classes restored from JSON // will add more object to canvas at canvas init. _this.insertAt(obj, index); }); - this.renderOnAddRemove = renderOnAddRemove; // remove parts i cannot set as options delete serialized.objects; delete serialized.backgroundImage; From 172d55001f8b0efd5bd05148b8999368cb44bc62 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Wed, 23 Mar 2022 14:05:01 +0200 Subject: [PATCH 05/55] Update misc.js --- src/util/misc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/misc.js b/src/util/misc.js index 30e6397bc0c..017491ef1c0 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -533,7 +533,7 @@ var abort; return new Promise(function (resolve, reject) { if (signal && signal.aborted) { - return reject(new DOMException('`signal` is in `aborted` state', 'ABORT_ERR')); + return reject(new DOMException('`options.signal` is in `aborted` state', 'ABORT_ERR')); } else if (signal) { abort = function () { From 65e69846c1d52abe7530388a9056470ff3b0ae35 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 08:17:46 +0200 Subject: [PATCH 06/55] parallel promise --- src/mixins/canvas_serialization.mixin.js | 36 ++++++++++++------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/mixins/canvas_serialization.mixin.js b/src/mixins/canvas_serialization.mixin.js index ce7bcbef0f3..ee661c5a4d3 100644 --- a/src/mixins/canvas_serialization.mixin.js +++ b/src/mixins/canvas_serialization.mixin.js @@ -5,7 +5,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @param {String|Object} json JSON string or object * @param {Function} [reviver] Method for further parsing of JSON elements, called after each fabric object created. * @return {Promise} instance - * @chainable * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#deserialization} * @see {@link http://jsfiddle.net/fabricjs/fmgXt/|jsFiddle demo} * @example loadFromJSON @@ -30,26 +29,27 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati : fabric.util.object.clone(json); var _this = this, - renderOnAddRemove = this.renderOnAddRemove; + renderOnAddRemove = this.renderOnAddRemove, + abortController = new AbortController(); this.renderOnAddRemove = false; - return fabric.util.enlivenObjects(serialized.objects || [], '', reviver) - .then(function(enlived) { - return fabric.util.enlivenObjectEnlivables({ - backgroundImage: serialized.backgroundImage, - backgroundColor: serialized.background, - overlayImage: serialized.overlayImage, - overlayColor: serialized.overlay, - clipPath: serialized.clipPath, - }) - .then(function(enlivedMap) { - _this.clear(); - _this.__setupCanvas(serialized, enlived); - _this.renderOnAddRemove = renderOnAddRemove; - _this.set(enlivedMap); - return _this; - }); + return Promise.all([ + fabric.util.enlivenObjects(serialized.objects || [], { reviver: reviver, signal: abortController.signal }), + fabric.util.enlivenObjectEnlivables({ + backgroundImage: serialized.backgroundImage, + backgroundColor: serialized.background, + overlayImage: serialized.overlayImage, + overlayColor: serialized.overlay, + clipPath: serialized.clipPath, + }) + ]) + .then(function (res) { + var enlived = res[0], enlivedMap = res[1]; + _this.clear(); + _this.__setupCanvas(serialized, enlived); + _this.renderOnAddRemove = renderOnAddRemove; + _this.set(enlivedMap); }); }, From b8f0ada5abd041168b43d69173d6de79189fd52d Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 08:22:24 +0200 Subject: [PATCH 07/55] live namespace --- src/shapes/image.class.js | 9 ++++---- src/util/misc.js | 44 ++++++++++++--------------------------- 2 files changed, 18 insertions(+), 35 deletions(-) diff --git a/src/shapes/image.class.js b/src/shapes/image.class.js index f4a1fc78ace..5e7e6c7b6f5 100644 --- a/src/shapes/image.class.js +++ b/src/shapes/image.class.js @@ -683,17 +683,18 @@ * @param {Object} object Object to create an instance from * @returns {Promise} */ - fabric.Image.fromObject = function(_object) { + fabric.Image.fromObject = function(_object, options) { var object = fabric.util.object.clone(_object), filters = object.filters, resizeFilter = object.resizeFilter; // the generic enliving will fail on filters for now delete object.resizeFilter; delete object.filters; + var imageOptions = Object.assign({ crossOrigin: _object.crossOrigin }, options); return Promise.all([ - fabric.util.loadImage(object.src, { crossOrigin: _object.crossOrigin }), - filters && fabric.util.enlivenObjects(filters, 'fabric.Image.filters'), - resizeFilter && fabric.util.enlivenObjects([resizeFilter], 'fabric.Image.filters'), + fabric.util.loadImage(object.src, imageOptions), + filters && fabric.util.enlivenObjects(filters, fabric.Image.filters), + resizeFilter && fabric.util.enlivenObjects([resizeFilter], fabric.Image.filters), fabric.util.enlivenObjectEnlivables(object), ]) .then(function(imgAndFilters) { diff --git a/src/util/misc.js b/src/util/misc.js index 017491ef1c0..e00aec73ac2 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -461,13 +461,13 @@ * Returns klass "Class" object of given namespace * @memberOf fabric.util * @param {String} type Type of object (eg. 'circle') - * @param {String} namespace Namespace to get klass "Class" object from + * @param {object} namespace Namespace to get klass "Class" object from * @return {Object} klass "Class" */ getKlass: function(type, namespace) { // capitalize first letter only type = fabric.util.string.camelize(type.charAt(0).toUpperCase() + type.slice(1)); - return fabric.util.resolveNamespace(namespace)[type]; + return (namespace || fabric)[type]; }, /** @@ -497,28 +497,6 @@ return attributes; }, - /** - * Returns object of given namespace - * @memberOf fabric.util - * @param {String} namespace Namespace string e.g. 'fabric.Image.filter' or 'fabric' - * @return {Object} Object for given namespace (default fabric) - */ - resolveNamespace: function(namespace) { - if (!namespace) { - return fabric; - } - - var parts = namespace.split('.'), - len = parts.length, i, - obj = global || fabric.window; - - for (i = 0; i < len; ++i) { - obj = obj[parts[i]]; - } - - return obj; - }, - /** * Loads image element from given url and resolve it, or catch. * @memberOf fabric.util @@ -537,6 +515,7 @@ } else if (signal) { abort = function () { + img.src = ''; reject(new DOMException('aborted by user', 'ABORT_ERR')); } signal.addEventListener('abort', abort); @@ -566,15 +545,18 @@ * @static * @memberOf fabric.util * @param {Object[]} objects Objects to enliven - * @param {String} namespace Namespace to get klass "Class" object from - * @param {Function} reviver Method for further parsing of object elements, + * @param {object} [options] + * @param {object} [options.namespace] Namespace to get klass "Class" object from + * @param {(serializedObj: object, instance: fabric.Object) => any} [options.reviver] Method for further parsing of object elements, * called after each fabric object created. + * @param {AbortSignal} [options.signal] */ - enlivenObjects: function(objects, namespace, reviver) { + enlivenObjects: function(objects, options) { + options = options || {}; return Promise.all(objects.map(function(obj) { - var klass = fabric.util.getKlass(obj.type, namespace); - return klass.fromObject(obj).then(function(fabricInstance) { - reviver && reviver(obj, fabricInstance); + var klass = fabric.util.getKlass(obj.type, options.namespace || fabric); + return klass.fromObject(obj, options).then(function(fabricInstance) { + options.reviver && options.reviver(obj, fabricInstance); return fabricInstance; }); })); @@ -586,7 +568,7 @@ * @returns {Promise} the input object with enlived values */ - enlivenObjectEnlivables: function (serializedObject) { + enlivenObjectEnlivables: function (serializedObject, options) { // enlive every possible property var promises = Object.values(serializedObject).map(function(value) { if (!value) { From c02b917bfce41530960d99dc7c197c0672fd5879 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 08:22:33 +0200 Subject: [PATCH 08/55] enliven options --- src/shapes/object.class.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/shapes/object.class.js b/src/shapes/object.class.js index 327fa7fedd2..fc4f478e3bc 100644 --- a/src/shapes/object.class.js +++ b/src/shapes/object.class.js @@ -1939,16 +1939,23 @@ * @type string[] */ - fabric.Object._fromObject = function(klass, object, extraParam) { + fabric.Object._fromObject = function(klass, object, extraParam, options) { var serializedObject = clone(object, true); - return fabric.util.enlivenObjectEnlivables(serializedObject).then(function(enlivedMap) { + return fabric.util.enlivenObjectEnlivables(serializedObject, options).then(function(enlivedMap) { var newObject = Object.assign(object, enlivedMap); return extraParam ? new klass(object[extraParam], newObject) : new klass(newObject); }); }; - fabric.Object.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Object, object); + /** + * + * @param {object} object + * @param {object} options + * @param {AbortSignal} options.signal + * @returns + */ + fabric.Object.fromObject = function(object, options) { + return fabric.Object._fromObject(fabric.Object, object, null, options); }; /** From c1fdfd4df7e2855336c465933bd126862561e93b Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 08:47:41 +0200 Subject: [PATCH 09/55] Update misc.js --- src/util/misc.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/util/misc.js b/src/util/misc.js index e00aec73ac2..8773382d864 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -507,8 +507,7 @@ * @param {Promise} img the loaded image. */ loadImage: function (url, options) { - var signal = options && options.signal; - var abort; + var abort, signal = options && options.signal; return new Promise(function (resolve, reject) { if (signal && signal.aborted) { return reject(new DOMException('`options.signal` is in `aborted` state', 'ABORT_ERR')); @@ -550,6 +549,7 @@ * @param {(serializedObj: object, instance: fabric.Object) => any} [options.reviver] Method for further parsing of object elements, * called after each fabric object created. * @param {AbortSignal} [options.signal] + * @returns {Promise} */ enlivenObjects: function(objects, options) { options = options || {}; @@ -564,11 +564,16 @@ /** * Creates corresponding fabric instances residing in an object, e.g. `clipPath` + * @static + * @memberOf fabric.util * @param {Object} object with properties to enlive ( fill, stroke, clipPath, path ) - * @returns {Promise} the input object with enlived values + * @param {object} [options] + * @param {AbortSignal} [options.signal] + * @returns {Promise<{[key:string]:fabric.Object|fabric.Pattern|fabric.Gradient|null}>} the input object with enlived values */ - enlivenObjectEnlivables: function (serializedObject, options) { + // make sure we don't pass other properties + var opts = { signal: options.signal }; // enlive every possible property var promises = Object.values(serializedObject).map(function(value) { if (!value) { @@ -578,12 +583,12 @@ return new fabric.Gradient(value); } if (value.type) { - return fabric.util.enlivenObjects([value]).then(function (enlived) { + return fabric.util.enlivenObjects([value], opts).then(function (enlived) { return enlived[0]; }); } if (value.source) { - return fabric.Pattern.fromObject(value); + return fabric.Pattern.fromObject(value, opts); } return value; }); From cc0539de121857f640f14dc97600f130de57c291 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 08:47:51 +0200 Subject: [PATCH 10/55] Update image.class.js --- src/shapes/image.class.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/shapes/image.class.js b/src/shapes/image.class.js index 5e7e6c7b6f5..95b74107bb3 100644 --- a/src/shapes/image.class.js +++ b/src/shapes/image.class.js @@ -690,12 +690,13 @@ // the generic enliving will fail on filters for now delete object.resizeFilter; delete object.filters; - var imageOptions = Object.assign({ crossOrigin: _object.crossOrigin }, options); + var imageOptions = Object.assign({}, options, { crossOrigin: _object.crossOrigin }), + filterOptions = Object.assign({}, options, { namespace: fabric.Image.filters }); return Promise.all([ fabric.util.loadImage(object.src, imageOptions), - filters && fabric.util.enlivenObjects(filters, fabric.Image.filters), - resizeFilter && fabric.util.enlivenObjects([resizeFilter], fabric.Image.filters), - fabric.util.enlivenObjectEnlivables(object), + filters && fabric.util.enlivenObjects(filters, filterOptions), + resizeFilter && fabric.util.enlivenObjects([resizeFilter], filterOptions), + fabric.util.enlivenObjectEnlivables(object, options), ]) .then(function(imgAndFilters) { object.filters = imgAndFilters[1] || []; From 3099a655474d6abb021c4fddbc82bffa502b800b Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 08:48:02 +0200 Subject: [PATCH 11/55] Update pattern.class.js --- src/pattern.class.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/pattern.class.js b/src/pattern.class.js index 4a6f6aa94e4..b9d2baa7099 100644 --- a/src/pattern.class.js +++ b/src/pattern.class.js @@ -175,9 +175,17 @@ } }); - fabric.Pattern.fromObject = function(object) { - var patternOptions = Object.assign({}, object); - return fabric.util.loadImage(object.source, { crossOrigin: object.crossOrigin }) + /** + * + * @param {object} object + * @param {object} options + * @param {AbortSignal} options.signal + * @returns + */ + fabric.Pattern.fromObject = function(object, options) { + var patternOptions = Object.assign({}, object), + imageOptions = Object.assign({ crossOrigin: object.crossOrigin }, options); + return fabric.util.loadImage(object.source, imageOptions) .then(function(img) { patternOptions.source = img; return new fabric.Pattern(patternOptions); From f46d83396c24b83b4fdbb514fff8d74594ec9551 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 08:59:38 +0200 Subject: [PATCH 12/55] aborting --- src/mixins/canvas_serialization.mixin.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/mixins/canvas_serialization.mixin.js b/src/mixins/canvas_serialization.mixin.js index ee661c5a4d3..0762e153c20 100644 --- a/src/mixins/canvas_serialization.mixin.js +++ b/src/mixins/canvas_serialization.mixin.js @@ -1,4 +1,15 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { + + /** + * Aborts instance's loading task ({@link fabric.Canvas#loadFromJSON} etc.) if exists + */ + abortLoadingTask: function () { + if (this.__abortController) { + this.__abortController.abort(); + delete this.__abortController; + } + }, + /** * Populates canvas with data from the specified JSON. * JSON format must conform to the one of {@link fabric.Canvas#toJSON} @@ -19,6 +30,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * }); */ loadFromJSON: function (json, reviver) { + this.abortLoadingTask(); if (!json) { return; } @@ -32,6 +44,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati renderOnAddRemove = this.renderOnAddRemove, abortController = new AbortController(); + this.__abortController = abortController; this.renderOnAddRemove = false; return Promise.all([ @@ -42,13 +55,14 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati overlayImage: serialized.overlayImage, overlayColor: serialized.overlay, clipPath: serialized.clipPath, - }) + }, { signal: abortController.signal }) ]) .then(function (res) { var enlived = res[0], enlivedMap = res[1]; _this.clear(); _this.__setupCanvas(serialized, enlived); _this.renderOnAddRemove = renderOnAddRemove; + delete _this.__abortController; _this.set(enlivedMap); }); }, From 54b01e5949be70d551fb530d495301c9a239bd54 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 09:27:48 +0200 Subject: [PATCH 13/55] aborting svg parsing --- src/parser.js | 11 +++++++++-- src/shapes/image.class.js | 7 ++++++- src/util/dom_request.js | 17 +++++++++++++++-- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/parser.js b/src/parser.js index cd729a08b7b..a168ec1a881 100644 --- a/src/parser.js +++ b/src/parser.js @@ -688,12 +688,15 @@ * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. * @param {Object} [parsingOptions] options for parsing document * @param {String} [parsingOptions.crossOrigin] crossOrigin settings + * @param {AbortSignal} [parsingOptions.signal] see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal */ fabric.parseSVGDocument = function(doc, callback, reviver, parsingOptions) { if (!doc) { return; } - + if (parsingOptions && parsingOptions.signal && parsingOptions.signal.aborted) { + throw new DOMException('`options.signal` is in `aborted` state', 'ABORT_ERR'); + } parseUseDirectives(doc); var svgUid = fabric.Object.__uid++, i, len, @@ -701,6 +704,7 @@ descendants = fabric.util.toArray(doc.getElementsByTagName('*')); options.crossOrigin = parsingOptions && parsingOptions.crossOrigin; options.svgUid = svgUid; + options.signal = parsingOptions && parsingOptions.signal; if (descendants.length === 0 && fabric.isLikelyNode) { // we're likely in node, where "o3-xml" library fails to gEBTN("*") @@ -1046,13 +1050,15 @@ * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. * @param {Object} [options] Object containing options for parsing * @param {String} [options.crossOrigin] crossOrigin crossOrigin setting to use for external resources + * @param {AbortSignal} [options.signal] handle aborting */ loadSVGFromURL: function(url, callback, reviver, options) { url = url.replace(/^\n\s*/, '').trim(); new fabric.util.request(url, { method: 'get', - onComplete: onComplete + onComplete: onComplete, + signal: options.signal }); function onComplete(r) { @@ -1077,6 +1083,7 @@ * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. * @param {Object} [options] Object containing options for parsing * @param {String} [options.crossOrigin] crossOrigin crossOrigin setting to use for external resources + * @param {AbortSignal} [options.signal] see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal */ loadSVGFromString: function(string, callback, reviver, options) { var parser = new fabric.window.DOMParser(), diff --git a/src/shapes/image.class.js b/src/shapes/image.class.js index 95b74107bb3..81e6e74c516 100644 --- a/src/shapes/image.class.js +++ b/src/shapes/image.class.js @@ -681,6 +681,8 @@ * Creates an instance of fabric.Image from its object representation * @static * @param {Object} object Object to create an instance from + * @param {object} [options] Options object + * @param {AbortSignal} [options.signal] see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal * @returns {Promise} */ fabric.Image.fromObject = function(_object, options) { @@ -709,7 +711,9 @@ * Creates an instance of fabric.Image from an URL string * @static * @param {String} url URL to create an image from - * @param {Object} [imgOptions] Options object + * @param {object} [imgOptions] Options object + * @param {string} [options.crossOrigin] cors value for the image loading, default to anonymous + * @param {AbortSignal} [options.signal] see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal * @returns {Promise} */ fabric.Image.fromURL = function(url, imgOptions) { @@ -734,6 +738,7 @@ * @static * @param {SVGElement} element Element to parse * @param {Object} [options] Options object + * @param {AbortSignal} [options.signal] see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal * @param {Function} callback Callback to execute when fabric.Image object is created * @return {fabric.Image} Instance of fabric.Image */ diff --git a/src/util/dom_request.js b/src/util/dom_request.js index 03a82ab7d42..50068c02afa 100644 --- a/src/util/dom_request.js +++ b/src/util/dom_request.js @@ -15,6 +15,7 @@ * @param {String} [options.method="GET"] * @param {String} [options.parameters] parameters to append to url in GET or in body * @param {String} [options.body] body to send with POST or PUT request + * @param {AbortSignal} [options.signal] * @param {Function} options.onComplete Callback to invoke when request is completed * @return {XMLHttpRequest} request */ @@ -22,13 +23,25 @@ options || (options = { }); var method = options.method ? options.method.toUpperCase() : 'GET', - onComplete = options.onComplete || function() { }, + onComplete = options.onComplete || function () { }, xhr = new fabric.window.XMLHttpRequest(), - body = options.body || options.parameters; + body = options.body || options.parameters, + signal = options.signal, + abort = abort = function () { + xhr.abort(); + }; + if (signal && signal.aborted) { + throw new DOMException('`options.signal` is in `aborted` state', 'ABORT_ERR'); + } + else if (signal) { + signal.addEventListener('abort', abort); + } + /** @ignore */ xhr.onreadystatechange = function() { if (xhr.readyState === 4) { + signal && signal.removeEventListener('abort', abort); onComplete(xhr); xhr.onreadystatechange = emptyFn; } From 585785959931647c0d101ff9a51f822979f1b5c1 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 10:35:02 +0200 Subject: [PATCH 14/55] bump node to support AbortController https://nodejs.org/api/globals.html#class-abortcontroller --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 654e6028a56..5d0122582ce 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "uglify-js": "3.3.x" }, "engines": { - "node": ">=14.0.0" + "node": ">=14.17.0" }, "main": "./dist/fabric.js", "dependencies": {} From 4ffefd6243b196685a78b8953a8474fa1d7622b9 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 10:35:15 +0200 Subject: [PATCH 15/55] listen to abort once --- src/util/dom_request.js | 2 +- src/util/misc.js | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/util/dom_request.js b/src/util/dom_request.js index 50068c02afa..c0ce604a63e 100644 --- a/src/util/dom_request.js +++ b/src/util/dom_request.js @@ -35,7 +35,7 @@ throw new DOMException('`options.signal` is in `aborted` state', 'ABORT_ERR'); } else if (signal) { - signal.addEventListener('abort', abort); + signal.addEventListener('abort', abort, { once: true }); } /** @ignore */ diff --git a/src/util/misc.js b/src/util/misc.js index 8773382d864..62aab47fa22 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -517,7 +517,7 @@ img.src = ''; reject(new DOMException('aborted by user', 'ABORT_ERR')); } - signal.addEventListener('abort', abort); + signal.addEventListener('abort', abort, { once: true }); } var img = fabric.util.createImage(); var done = function() { @@ -572,8 +572,6 @@ * @returns {Promise<{[key:string]:fabric.Object|fabric.Pattern|fabric.Gradient|null}>} the input object with enlived values */ enlivenObjectEnlivables: function (serializedObject, options) { - // make sure we don't pass other properties - var opts = { signal: options.signal }; // enlive every possible property var promises = Object.values(serializedObject).map(function(value) { if (!value) { @@ -583,12 +581,12 @@ return new fabric.Gradient(value); } if (value.type) { - return fabric.util.enlivenObjects([value], opts).then(function (enlived) { + return fabric.util.enlivenObjects([value], options).then(function (enlived) { return enlived[0]; }); } if (value.source) { - return fabric.Pattern.fromObject(value, opts); + return fabric.Pattern.fromObject(value, options); } return value; }); From aaf3f5bc253919ad788230f56de8979951afa6fb Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 10:44:32 +0200 Subject: [PATCH 16/55] bump node tests --- .github/workflows/node-unit-tests.yml | 2 +- .github/workflows/visual-node.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/node-unit-tests.yml b/.github/workflows/node-unit-tests.yml index 895f16497d0..95fe55e0d71 100644 --- a/.github/workflows/node-unit-tests.yml +++ b/.github/workflows/node-unit-tests.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [14.x, 16.x] + node-version: [14.17.0, 16.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/visual-node.yml b/.github/workflows/visual-node.yml index 82080df9c8f..d8add20b07b 100644 --- a/.github/workflows/visual-node.yml +++ b/.github/workflows/visual-node.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [14.x, 16.x] + node-version: [14.17.0, 16.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - uses: actions/checkout@v2 From da720f54541c6fe5ca806fc458ebf689919628c8 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 10:57:01 +0200 Subject: [PATCH 17/55] lint + JSDOC --- src/parser.js | 4 ++-- src/pattern.class.js | 12 ++++++------ src/shapes/image.class.js | 8 ++++---- src/shapes/object.class.js | 25 ++++++++++++++----------- src/util/dom_request.js | 4 ++-- src/util/misc.js | 14 +++++++------- 6 files changed, 35 insertions(+), 32 deletions(-) diff --git a/src/parser.js b/src/parser.js index a168ec1a881..1d7e953c648 100644 --- a/src/parser.js +++ b/src/parser.js @@ -1050,7 +1050,7 @@ * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. * @param {Object} [options] Object containing options for parsing * @param {String} [options.crossOrigin] crossOrigin crossOrigin setting to use for external resources - * @param {AbortSignal} [options.signal] handle aborting + * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal */ loadSVGFromURL: function(url, callback, reviver, options) { @@ -1083,7 +1083,7 @@ * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. * @param {Object} [options] Object containing options for parsing * @param {String} [options.crossOrigin] crossOrigin crossOrigin setting to use for external resources - * @param {AbortSignal} [options.signal] see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal + * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal */ loadSVGFromString: function(string, callback, reviver, options) { var parser = new fabric.window.DOMParser(), diff --git a/src/pattern.class.js b/src/pattern.class.js index b9d2baa7099..f98bdd89f46 100644 --- a/src/pattern.class.js +++ b/src/pattern.class.js @@ -176,15 +176,15 @@ }); /** - * - * @param {object} object - * @param {object} options - * @param {AbortSignal} options.signal - * @returns + * + * @param {object} object + * @param {object} [options] + * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal + * @returns */ fabric.Pattern.fromObject = function(object, options) { var patternOptions = Object.assign({}, object), - imageOptions = Object.assign({ crossOrigin: object.crossOrigin }, options); + imageOptions = Object.assign({ crossOrigin: object.crossOrigin }, options); return fabric.util.loadImage(object.source, imageOptions) .then(function(img) { patternOptions.source = img; diff --git a/src/shapes/image.class.js b/src/shapes/image.class.js index 81e6e74c516..e646843fc9c 100644 --- a/src/shapes/image.class.js +++ b/src/shapes/image.class.js @@ -682,7 +682,7 @@ * @static * @param {Object} object Object to create an instance from * @param {object} [options] Options object - * @param {AbortSignal} [options.signal] see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal + * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal * @returns {Promise} */ fabric.Image.fromObject = function(_object, options) { @@ -693,7 +693,7 @@ delete object.resizeFilter; delete object.filters; var imageOptions = Object.assign({}, options, { crossOrigin: _object.crossOrigin }), - filterOptions = Object.assign({}, options, { namespace: fabric.Image.filters }); + filterOptions = Object.assign({}, options, { namespace: fabric.Image.filters }); return Promise.all([ fabric.util.loadImage(object.src, imageOptions), filters && fabric.util.enlivenObjects(filters, filterOptions), @@ -713,7 +713,7 @@ * @param {String} url URL to create an image from * @param {object} [imgOptions] Options object * @param {string} [options.crossOrigin] cors value for the image loading, default to anonymous - * @param {AbortSignal} [options.signal] see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal + * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal * @returns {Promise} */ fabric.Image.fromURL = function(url, imgOptions) { @@ -738,7 +738,7 @@ * @static * @param {SVGElement} element Element to parse * @param {Object} [options] Options object - * @param {AbortSignal} [options.signal] see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal + * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal * @param {Function} callback Callback to execute when fabric.Image object is created * @return {fabric.Image} Instance of fabric.Image */ diff --git a/src/shapes/object.class.js b/src/shapes/object.class.js index fc4f478e3bc..75b6e305fb9 100644 --- a/src/shapes/object.class.js +++ b/src/shapes/object.class.js @@ -1932,13 +1932,14 @@ fabric.Object.NUM_FRACTION_DIGITS = 2; /** - * Defines which properties should be enlivened from the object passed to {@link fabric.Object._fromObject} - * @static - * @memberOf fabric.Object - * @constant - * @type string[] + * + * @param {Function} klass + * @param {object} object + * @param {string} [extraParam] property to pass as first argument to the constructor + * @param {object} [options] + * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal + * @returns {Promise} */ - fabric.Object._fromObject = function(klass, object, extraParam, options) { var serializedObject = clone(object, true); return fabric.util.enlivenObjectEnlivables(serializedObject, options).then(function(enlivedMap) { @@ -1948,11 +1949,13 @@ }; /** - * - * @param {object} object - * @param {object} options - * @param {AbortSignal} options.signal - * @returns + * + * @static + * @memberOf fabric.Object + * @param {object} object + * @param {object} [options] + * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal + * @returns {Promise} */ fabric.Object.fromObject = function(object, options) { return fabric.Object._fromObject(fabric.Object, object, null, options); diff --git a/src/util/dom_request.js b/src/util/dom_request.js index c0ce604a63e..bd0559215c4 100644 --- a/src/util/dom_request.js +++ b/src/util/dom_request.js @@ -15,7 +15,7 @@ * @param {String} [options.method="GET"] * @param {String} [options.parameters] parameters to append to url in GET or in body * @param {String} [options.body] body to send with POST or PUT request - * @param {AbortSignal} [options.signal] + * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal * @param {Function} options.onComplete Callback to invoke when request is completed * @return {XMLHttpRequest} request */ @@ -37,7 +37,7 @@ else if (signal) { signal.addEventListener('abort', abort, { once: true }); } - + /** @ignore */ xhr.onreadystatechange = function() { if (xhr.readyState === 4) { diff --git a/src/util/misc.js b/src/util/misc.js index 62aab47fa22..c7f83724298 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -1,4 +1,4 @@ -(function(global) { +(function() { var sqrt = Math.sqrt, atan2 = Math.atan2, @@ -503,7 +503,7 @@ * @param {String} url URL representing an image * @param {Object} [options] image loading options * @param {string} [options.crossOrigin] cors value for the image loading, default to anonymous - * @param {AbortSignal} [options.signal] see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal + * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal * @param {Promise} img the loaded image. */ loadImage: function (url, options) { @@ -516,7 +516,7 @@ abort = function () { img.src = ''; reject(new DOMException('aborted by user', 'ABORT_ERR')); - } + }; signal.addEventListener('abort', abort, { once: true }); } var img = fabric.util.createImage(); @@ -544,11 +544,11 @@ * @static * @memberOf fabric.util * @param {Object[]} objects Objects to enliven - * @param {object} [options] + * @param {object} [options] * @param {object} [options.namespace] Namespace to get klass "Class" object from * @param {(serializedObj: object, instance: fabric.Object) => any} [options.reviver] Method for further parsing of object elements, * called after each fabric object created. - * @param {AbortSignal} [options.signal] + * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal * @returns {Promise} */ enlivenObjects: function(objects, options) { @@ -567,8 +567,8 @@ * @static * @memberOf fabric.util * @param {Object} object with properties to enlive ( fill, stroke, clipPath, path ) - * @param {object} [options] - * @param {AbortSignal} [options.signal] + * @param {object} [options] + * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal * @returns {Promise<{[key:string]:fabric.Object|fabric.Pattern|fabric.Gradient|null}>} the input object with enlived values */ enlivenObjectEnlivables: function (serializedObject, options) { From 82c4e9e19b53295f53fd8814be6da789617297fb Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 11:00:33 +0200 Subject: [PATCH 18/55] Update parser.js --- src/parser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser.js b/src/parser.js index 1d7e953c648..70f7f1ae918 100644 --- a/src/parser.js +++ b/src/parser.js @@ -1058,7 +1058,7 @@ new fabric.util.request(url, { method: 'get', onComplete: onComplete, - signal: options.signal + signal: options && options.signal }); function onComplete(r) { From b1470ddbd923bd522cca3a17d25a56d4f104b3e2 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 11:09:37 +0200 Subject: [PATCH 19/55] feat(Image): abort `setSrc` --- src/shapes/image.class.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/shapes/image.class.js b/src/shapes/image.class.js index e646843fc9c..5cc9f15d212 100644 --- a/src/shapes/image.class.js +++ b/src/shapes/image.class.js @@ -366,6 +366,16 @@ } }, + /** + * Aborts pending image loading task + */ + abortLoadingTask: function () { + if (this.__abortController) { + this.__abortController.abort(); + delete this.__abortController; + } + }, + /** * Sets source of an image * @param {String} src Source string (URL) @@ -376,9 +386,13 @@ */ setSrc: function(src, options) { var _this = this; - return fabric.util.loadImage(src, options).then(function(img) { + this.abortLoadingTask(); + this.__abortController = new AbortController(); + var opts = Object.assign({}, options, { signal: this.__abortController.signal }); + return fabric.util.loadImage(src, opts).then(function(img) { _this.setElement(img, options); _this._setWidthHeight(); + delete this.__abortController; return _this; }); }, From 20e8e87ccff16ee39ee4605486796c356580b575 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 11:15:06 +0200 Subject: [PATCH 20/55] Update .eslintrc.json --- .eslintrc.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.eslintrc.json b/.eslintrc.json index f3d848c6348..4e8cd083eb2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -12,7 +12,8 @@ "Buffer": true, "process": true, "QUnit": true, - "assert": true + "assert": true, + "AbortController": true }, "rules": { "semi": 2, From ea049c97b95c55ab9339682b15f44c1cc9573433 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 11:17:51 +0200 Subject: [PATCH 21/55] Update canvas_serialization.mixin.js --- src/mixins/canvas_serialization.mixin.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mixins/canvas_serialization.mixin.js b/src/mixins/canvas_serialization.mixin.js index 0762e153c20..40197d4d16b 100644 --- a/src/mixins/canvas_serialization.mixin.js +++ b/src/mixins/canvas_serialization.mixin.js @@ -32,7 +32,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati loadFromJSON: function (json, reviver) { this.abortLoadingTask(); if (!json) { - return; + return Promise.reject(new Error('fabric.js: `json` is undefined')); } // serialize if it wasn't already @@ -64,6 +64,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati _this.renderOnAddRemove = renderOnAddRemove; delete _this.__abortController; _this.set(enlivedMap); + return _this; }); }, From 92f53360430dd174adec92851acb040a12de8b7d Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 11:32:29 +0200 Subject: [PATCH 22/55] remove `resolveNamespace` --- test/unit/util.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/unit/util.js b/test/unit/util.js index 8a224c5f7d4..fdbd7b899ce 100644 --- a/test/unit/util.js +++ b/test/unit/util.js @@ -691,12 +691,6 @@ assert.equal(fabric.util.getKlass('Sepia2', 'fabric.Image.filters'), fabric.Image.filters.Sepia2); }); - QUnit.test('resolveNamespace', function(assert) { - assert.equal(fabric.util.resolveNamespace('fabric'), fabric); - assert.equal(fabric.util.resolveNamespace('fabric.Image'), fabric.Image); - assert.equal(fabric.util.resolveNamespace('fabric.Image.filters'), fabric.Image.filters); - }); - QUnit.test('clearFabricFontCache', function(assert) { assert.ok(typeof fabric.util.clearFabricFontCache === 'function'); fabric.charWidthsCache = { arial: { some: 'cache'}, helvetica: { some: 'cache'} }; From 7510ab1be0c7f943fd34f5ce355231968ae4ba60 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 11:41:19 +0200 Subject: [PATCH 23/55] Update pattern.class.js --- src/pattern.class.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pattern.class.js b/src/pattern.class.js index f98bdd89f46..6e35049b099 100644 --- a/src/pattern.class.js +++ b/src/pattern.class.js @@ -184,7 +184,7 @@ */ fabric.Pattern.fromObject = function(object, options) { var patternOptions = Object.assign({}, object), - imageOptions = Object.assign({ crossOrigin: object.crossOrigin }, options); + imageOptions = Object.assign({}, options, { crossOrigin: object.crossOrigin }); return fabric.util.loadImage(object.source, imageOptions) .then(function(img) { patternOptions.source = img; From 0565006b02407f372c5f250f21d3cbaeb4400f6d Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 11:45:36 +0200 Subject: [PATCH 24/55] Update misc.js --- src/util/misc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/util/misc.js b/src/util/misc.js index c7f83724298..d0f24864757 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -531,6 +531,7 @@ else { img.onload = done; img.onerror = function () { + signal && abort && signal.removeEventListener('abort', abort); reject(new Error('Error loading ' + img.src)); }; options && options.crossOrigin && (img.crossOrigin = options.crossOrigin); From 88b3f0c335d4aeeef4c09b91d3ddf391fc730f3e Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 15:43:34 +0200 Subject: [PATCH 25/55] consolidate options object --- src/shapes/itext.class.js | 2 +- src/shapes/line.class.js | 2 +- src/shapes/object.class.js | 8 ++++---- src/shapes/path.class.js | 2 +- src/shapes/polygon.class.js | 2 +- src/shapes/polyline.class.js | 2 +- src/shapes/text.class.js | 2 +- src/shapes/textbox.class.js | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/shapes/itext.class.js b/src/shapes/itext.class.js index b4e66b3ad63..5b8b0f878b1 100644 --- a/src/shapes/itext.class.js +++ b/src/shapes/itext.class.js @@ -500,6 +500,6 @@ * @returns {Promise} */ fabric.IText.fromObject = function(object) { - return fabric.Object._fromObject(fabric.IText, object, 'text'); + return fabric.Object._fromObject(fabric.IText, object, { extraParam: 'text' }); }; })(); diff --git a/src/shapes/line.class.js b/src/shapes/line.class.js index c64beef282e..ee4ca2315a2 100644 --- a/src/shapes/line.class.js +++ b/src/shapes/line.class.js @@ -289,7 +289,7 @@ fabric.Line.fromObject = function(object) { var options = clone(object, true); options.points = [object.x1, object.y1, object.x2, object.y2]; - return fabric.Object._fromObject(fabric.Line, options, 'points').then(function(fabricLine) { + return fabric.Object._fromObject(fabric.Line, options, { extraParam: 'points' }).then(function(fabricLine) { delete fabricLine.points; return fabricLine; }); diff --git a/src/shapes/object.class.js b/src/shapes/object.class.js index 75b6e305fb9..80bb4ca4d74 100644 --- a/src/shapes/object.class.js +++ b/src/shapes/object.class.js @@ -1935,16 +1935,16 @@ * * @param {Function} klass * @param {object} object - * @param {string} [extraParam] property to pass as first argument to the constructor * @param {object} [options] + * @param {string} [options.extraParam] property to pass as first argument to the constructor * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal * @returns {Promise} */ - fabric.Object._fromObject = function(klass, object, extraParam, options) { + fabric.Object._fromObject = function(klass, object, options) { var serializedObject = clone(object, true); return fabric.util.enlivenObjectEnlivables(serializedObject, options).then(function(enlivedMap) { var newObject = Object.assign(object, enlivedMap); - return extraParam ? new klass(object[extraParam], newObject) : new klass(newObject); + return options && options.extraParam ? new klass(object[options.extraParam], newObject) : new klass(newObject); }); }; @@ -1958,7 +1958,7 @@ * @returns {Promise} */ fabric.Object.fromObject = function(object, options) { - return fabric.Object._fromObject(fabric.Object, object, null, options); + return fabric.Object._fromObject(fabric.Object, object, options); }; /** diff --git a/src/shapes/path.class.js b/src/shapes/path.class.js index bb7b7382f3b..f4b5e29ad6c 100644 --- a/src/shapes/path.class.js +++ b/src/shapes/path.class.js @@ -335,7 +335,7 @@ * @returns {Promise} */ fabric.Path.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Path, object, 'path'); + return fabric.Object._fromObject(fabric.Path, object, { extraParam: 'path' }); }; /* _FROM_SVG_START_ */ diff --git a/src/shapes/polygon.class.js b/src/shapes/polygon.class.js index dc127b3aa06..590a027afdf 100644 --- a/src/shapes/polygon.class.js +++ b/src/shapes/polygon.class.js @@ -74,7 +74,7 @@ * @returns {Promise} */ fabric.Polygon.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Polygon, object, 'points'); + return fabric.Object._fromObject(fabric.Polygon, object, { extraParam: 'points' }); }; })(typeof exports !== 'undefined' ? exports : this); diff --git a/src/shapes/polyline.class.js b/src/shapes/polyline.class.js index 5d0b7b85b36..82a7a12eef5 100644 --- a/src/shapes/polyline.class.js +++ b/src/shapes/polyline.class.js @@ -262,7 +262,7 @@ * @returns {Promise} */ fabric.Polyline.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Polyline, object, 'points'); + return fabric.Object._fromObject(fabric.Polyline, object, { extraParam: 'points' }); }; })(typeof exports !== 'undefined' ? exports : this); diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index 2bb6bc2e5f7..97a13e492bf 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -1702,7 +1702,7 @@ * @returns {Promise} */ fabric.Text.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Text, object, 'text'); + return fabric.Object._fromObject(fabric.Text, object, { extraParam: 'text' }); }; fabric.Text.genericFonts = ['sans-serif', 'serif', 'cursive', 'fantasy', 'monospace']; diff --git a/src/shapes/textbox.class.js b/src/shapes/textbox.class.js index 06cf7982cfc..e1b7c01b871 100644 --- a/src/shapes/textbox.class.js +++ b/src/shapes/textbox.class.js @@ -453,6 +453,6 @@ * @returns {Promise} */ fabric.Textbox.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Textbox, object, 'text'); + return fabric.Object._fromObject(fabric.Textbox, object, { extraParam: 'text' }); }; })(typeof exports !== 'undefined' ? exports : this); From 3e0f7b6aaae4030d2d30b0abd42531aa123e7e35 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 15:44:33 +0200 Subject: [PATCH 26/55] abort image filter loading --- src/filters/blendimage_filter.class.js | 8 +++++--- src/filters/composed_filter.class.js | 9 +++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/filters/blendimage_filter.class.js b/src/filters/blendimage_filter.class.js index 5ff2d480da5..478a0b94892 100644 --- a/src/filters/blendimage_filter.class.js +++ b/src/filters/blendimage_filter.class.js @@ -232,11 +232,13 @@ /** * Create filter instance from an object representation * @static - * @param {Object} object Object to create an instance from + * @param {oject} object Object to create an instance from + * @param {object} [options] + * @param {AbortSignal} [options.signal] handle aborting image loading, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal * @returns {Promise} */ - fabric.Image.filters.BlendImage.fromObject = function(object) { - return fabric.Image.fromObject(object.image).then(function(image) { + fabric.Image.filters.BlendImage.fromObject = function(object, options) { + return fabric.Image.fromObject(object.image, options).then(function(image) { var options = fabric.util.object.clone(object); options.image = image; return new fabric.Image.filters.BlendImage(options); diff --git a/src/filters/composed_filter.class.js b/src/filters/composed_filter.class.js index dc42aa71f04..617b396ca65 100644 --- a/src/filters/composed_filter.class.js +++ b/src/filters/composed_filter.class.js @@ -59,11 +59,16 @@ /** * Deserialize a JSON definition of a ComposedFilter into a concrete instance. + * @static + * @param {oject} object Object to create an instance from + * @param {object} [options] + * @param {AbortSignal} [options.signal] handle aborting `BlendImage` filter loading, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal + * @returns {Promise} */ - fabric.Image.filters.Composed.fromObject = function(object) { + fabric.Image.filters.Composed.fromObject = function(object, options) { var filters = object.subFilters || []; return Promise.all(filters.map(function(filter) { - return fabric.Image.filters[filter.type].fromObject(filter); + return fabric.Image.filters[filter.type].fromObject(filter, options); })).then(function(enlivedFilters) { return new fabric.Image.filters.Composed({ subFilters: enlivedFilters }); }); From 9f82c17bf9cb6026ce782cf5a9489602928420f9 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 15:59:25 +0200 Subject: [PATCH 27/55] fire `loading:aborted` event --- src/mixins/canvas_serialization.mixin.js | 7 ++++++- src/static_canvas.class.js | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/mixins/canvas_serialization.mixin.js b/src/mixins/canvas_serialization.mixin.js index 40197d4d16b..b6d47f4a10e 100644 --- a/src/mixins/canvas_serialization.mixin.js +++ b/src/mixins/canvas_serialization.mixin.js @@ -2,12 +2,15 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Aborts instance's loading task ({@link fabric.Canvas#loadFromJSON} etc.) if exists + * @returns {boolean} true if aborted */ abortLoadingTask: function () { if (this.__abortController) { this.__abortController.abort(); delete this.__abortController; + return true; } + return false; }, /** @@ -30,7 +33,9 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * }); */ loadFromJSON: function (json, reviver) { - this.abortLoadingTask(); + if (this.abortLoadingTask()) { + this.fire('loading:aborted', { from: 'json' }); + } if (!json) { return Promise.reject(new Error('fabric.js: `json` is undefined')); } diff --git a/src/static_canvas.class.js b/src/static_canvas.class.js index 8e775e2dc13..4762ae2c43f 100644 --- a/src/static_canvas.class.js +++ b/src/static_canvas.class.js @@ -31,6 +31,7 @@ * @fires canvas:cleared * @fires object:added * @fires object:removed + * @fires loading:aborted */ fabric.StaticCanvas = fabric.util.createClass(fabric.CommonMethods, /** @lends fabric.StaticCanvas.prototype */ { From bb26215bfafcfb28ee944e7ac97beec8919c79d2 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 15:59:33 +0200 Subject: [PATCH 28/55] Update object.class.js --- src/shapes/object.class.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shapes/object.class.js b/src/shapes/object.class.js index 80bb4ca4d74..b7be9fb8665 100644 --- a/src/shapes/object.class.js +++ b/src/shapes/object.class.js @@ -1932,9 +1932,9 @@ fabric.Object.NUM_FRACTION_DIGITS = 2; /** - * - * @param {Function} klass - * @param {object} object + * + * @param {Function} klass + * @param {object} object * @param {object} [options] * @param {string} [options.extraParam] property to pass as first argument to the constructor * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal From b3a0ea78dc0e54c06f7f031920c42976af3c6b8d Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 16:23:10 +0200 Subject: [PATCH 29/55] disposing after aborting --- src/util/misc.js | 94 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 65 insertions(+), 29 deletions(-) diff --git a/src/util/misc.js b/src/util/misc.js index d0f24864757..2605e1d748b 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -554,13 +554,29 @@ */ enlivenObjects: function(objects, options) { options = options || {}; - return Promise.all(objects.map(function(obj) { - var klass = fabric.util.getKlass(obj.type, options.namespace || fabric); - return klass.fromObject(obj, options).then(function(fabricInstance) { - options.reviver && options.reviver(obj, fabricInstance); - return fabricInstance; - }); - })); + var instances = [], signal = options && options.signal; + return new Promise(function (resolve, reject) { + signal && signal.addEventListener('abort', reject, { once: true }); + Promise.all(objects.map(function (obj) { + var klass = fabric.util.getKlass(obj.type, options.namespace || fabric); + return klass.fromObject(obj, options).then(function (fabricInstance) { + options.reviver && options.reviver(obj, fabricInstance); + instances.push(fabricInstance); + return fabricInstance; + }); + })) + .then(resolve) + .catch(function (error) { + // cleanup + instances.forEach(function (instance) { + instance.dispose && instance.dispose(); + }); + reject(error); + }) + .finally(function () { + signal && signal.removeEventListener('abort', reject); + }); + }); }, /** @@ -573,30 +589,50 @@ * @returns {Promise<{[key:string]:fabric.Object|fabric.Pattern|fabric.Gradient|null}>} the input object with enlived values */ enlivenObjectEnlivables: function (serializedObject, options) { - // enlive every possible property - var promises = Object.values(serializedObject).map(function(value) { - if (!value) { + var instances = [], signal = options && options.signal; + return new Promise(function (resolve, reject) { + signal && signal.addEventListener('abort', reject, { once: true }); + // enlive every possible property + var promises = Object.values(serializedObject).map(function (value) { + if (!value) { + return value; + } + if (value.colorStops) { + return new fabric.Gradient(value); + } + if (value.type) { + return fabric.util.enlivenObjects([value], options).then(function (enlived) { + var instance = enlived[0]; + instances.push(instance); + return instance; + }); + } + if (value.source) { + return fabric.Pattern.fromObject(value, options).then(function (pattern) { + instances.push(pattern); + return pattern; + }); + } return value; - } - if (value.colorStops) { - return new fabric.Gradient(value); - } - if (value.type) { - return fabric.util.enlivenObjects([value], options).then(function (enlived) { - return enlived[0]; + }); + var keys = Object.keys(serializedObject); + Promise.all(promises).then(function (enlived) { + return enlived.reduce(function (acc, instance, index) { + acc[keys[index]] = instance; + return acc; + }, {}); + }) + .then(resolve) + .catch(function (error) { + // cleanup + instances.forEach(function (instance) { + instance.dispose && instance.dispose(); + }); + reject(error); + }) + .finally(function () { + signal && signal.removeEventListener('abort', reject); }); - } - if (value.source) { - return fabric.Pattern.fromObject(value, options); - } - return value; - }); - var keys = Object.keys(serializedObject); - return Promise.all(promises).then(function(enlived) { - return enlived.reduce(function(acc, instance, index) { - acc[keys[index]] = instance; - return acc; - }, {}); }); }, From fc5571391c09042af8f40094423130f35834084d Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 17:22:25 +0200 Subject: [PATCH 30/55] remove DOMException --- src/parser.js | 2 +- src/util/dom_request.js | 2 +- src/util/misc.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/parser.js b/src/parser.js index 70f7f1ae918..fde36255df2 100644 --- a/src/parser.js +++ b/src/parser.js @@ -695,7 +695,7 @@ return; } if (parsingOptions && parsingOptions.signal && parsingOptions.signal.aborted) { - throw new DOMException('`options.signal` is in `aborted` state', 'ABORT_ERR'); + throw new Error('`options.signal` is in `aborted` state'); } parseUseDirectives(doc); diff --git a/src/util/dom_request.js b/src/util/dom_request.js index bd0559215c4..798025e92c3 100644 --- a/src/util/dom_request.js +++ b/src/util/dom_request.js @@ -32,7 +32,7 @@ }; if (signal && signal.aborted) { - throw new DOMException('`options.signal` is in `aborted` state', 'ABORT_ERR'); + throw new Error('`options.signal` is in `aborted` state'); } else if (signal) { signal.addEventListener('abort', abort, { once: true }); diff --git a/src/util/misc.js b/src/util/misc.js index 2605e1d748b..51c314d5cfa 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -510,12 +510,12 @@ var abort, signal = options && options.signal; return new Promise(function (resolve, reject) { if (signal && signal.aborted) { - return reject(new DOMException('`options.signal` is in `aborted` state', 'ABORT_ERR')); + return reject(new Error('`options.signal` is in `aborted` state')); } else if (signal) { abort = function () { img.src = ''; - reject(new DOMException('aborted by user', 'ABORT_ERR')); + reject(new Error('aborted by user')); }; signal.addEventListener('abort', abort, { once: true }); } From e4d38829fa9d3a90c6ac05bc92422a9fd5f8d60d Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 17:27:41 +0200 Subject: [PATCH 31/55] Update canvas_static.js --- test/unit/canvas_static.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/unit/canvas_static.js b/test/unit/canvas_static.js index bbf050f209c..d124de1c08e 100644 --- a/test/unit/canvas_static.js +++ b/test/unit/canvas_static.js @@ -1278,6 +1278,26 @@ }); }); + QUnit.test('abort loadFromJSON with image background and color', function (assert) { + var done = assert.async(); + assert.expect(5); + var serialized = JSON.parse(PATH_JSON); + serialized.background = 'green'; + serialized.backgroundImage = { "type": "image", "originX": "left", "originY": "top", "left": 13.6, "top": -1.4, "width": 3000, "height": 3351, "fill": "rgb(0,0,0)", "stroke": null, "strokeWidth": 0, "strokeDashArray": null, "strokeLineCap": "butt", "strokeDashOffset": 0, "strokeLineJoin": "miter", "strokeMiterLimit": 4, "scaleX": 0.05, "scaleY": 0.05, "angle": 0, "flipX": false, "flipY": false, "opacity": 1, "shadow": null, "visible": true, "backgroundColor": "", "fillRule": "nonzero", "globalCompositeOperation": "source-over", "skewX": 0, "skewY": 0, "src": IMG_SRC, "filters": [], "crossOrigin": "" }; + canvas.on('loading:aborted', function (opt) { + assert.equal(opt.from, 'json'); + }); + canvas.loadFromJSON(serialized).catch(function (err) { + assert.equal(err.type, 'abort'); + }); + canvas.loadFromJSON(serialized).then(function () { + assert.ok(!canvas.isEmpty(), 'canvas is not empty'); + assert.equal(canvas.backgroundColor, 'green'); + assert.ok(canvas.backgroundImage instanceof fabric.Image); + done(); + }); + }); + QUnit.test('loadFromJSON custom properties', function(assert) { var done = assert.async(); var rect = new fabric.Rect({ width: 10, height: 20 }); From 131513c6eef5579b8f311a7838c0cbcbdf651890 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 17:30:35 +0200 Subject: [PATCH 32/55] Update image.class.js --- src/shapes/image.class.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shapes/image.class.js b/src/shapes/image.class.js index 5cc9f15d212..58d372f16c3 100644 --- a/src/shapes/image.class.js +++ b/src/shapes/image.class.js @@ -392,7 +392,7 @@ return fabric.util.loadImage(src, opts).then(function(img) { _this.setElement(img, options); _this._setWidthHeight(); - delete this.__abortController; + delete _this.__abortController; return _this; }); }, From 32ec3c50a52b17ce878936ba61f12d58abacf111 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 17:37:04 +0200 Subject: [PATCH 33/55] Update canvas_static.js --- test/unit/canvas_static.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/unit/canvas_static.js b/test/unit/canvas_static.js index d124de1c08e..fd17f180fba 100644 --- a/test/unit/canvas_static.js +++ b/test/unit/canvas_static.js @@ -1288,7 +1288,7 @@ assert.equal(opt.from, 'json'); }); canvas.loadFromJSON(serialized).catch(function (err) { - assert.equal(err.type, 'abort'); + assert.equal(err.type, 'abort', 'should be an error object'); }); canvas.loadFromJSON(serialized).then(function () { assert.ok(!canvas.isEmpty(), 'canvas is not empty'); @@ -1298,6 +1298,21 @@ }); }); + + QUnit.test('imperative abort loadFromJSON', function (assert) { + var done = assert.async(); + assert.expect(3); + var serialized = JSON.parse(PATH_JSON); + serialized.background = 'green'; + serialized.backgroundImage = { "type": "image", "originX": "left", "originY": "top", "left": 13.6, "top": -1.4, "width": 3000, "height": 3351, "fill": "rgb(0,0,0)", "stroke": null, "strokeWidth": 0, "strokeDashArray": null, "strokeLineCap": "butt", "strokeDashOffset": 0, "strokeLineJoin": "miter", "strokeMiterLimit": 4, "scaleX": 0.05, "scaleY": 0.05, "angle": 0, "flipX": false, "flipY": false, "opacity": 1, "shadow": null, "visible": true, "backgroundColor": "", "fillRule": "nonzero", "globalCompositeOperation": "source-over", "skewX": 0, "skewY": 0, "src": IMG_SRC, "filters": [], "crossOrigin": "" }; + canvas.loadFromJSON(serialized).catch(function (err) { + assert.equal(err.type, 'abort'); + }); + assert.ok(canvas.abortLoadingTask(), 'should return true because loading was aborted'); + assert.ok(canvas.isEmpty(), 'canvas is empty'); + done(); + }); + QUnit.test('loadFromJSON custom properties', function(assert) { var done = assert.async(); var rect = new fabric.Rect({ width: 10, height: 20 }); From a21a4a473daae4ecbcaa18c30cbd2f9bda2335d2 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 19:14:35 +0200 Subject: [PATCH 34/55] fix(): concurrency error --- src/mixins/canvas_serialization.mixin.js | 5 ++++- src/shapes/image.class.js | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/mixins/canvas_serialization.mixin.js b/src/mixins/canvas_serialization.mixin.js index b6d47f4a10e..8cc3d212d9f 100644 --- a/src/mixins/canvas_serialization.mixin.js +++ b/src/mixins/canvas_serialization.mixin.js @@ -67,9 +67,12 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati _this.clear(); _this.__setupCanvas(serialized, enlived); _this.renderOnAddRemove = renderOnAddRemove; - delete _this.__abortController; _this.set(enlivedMap); return _this; + }).finally(function () { + if (abortController === _this.__abortController) { + delete _this.__abortController; + } }); }, diff --git a/src/shapes/image.class.js b/src/shapes/image.class.js index 58d372f16c3..8e080ab53ae 100644 --- a/src/shapes/image.class.js +++ b/src/shapes/image.class.js @@ -368,12 +368,15 @@ /** * Aborts pending image loading task + * @returns {boolean} true if aborted */ abortLoadingTask: function () { if (this.__abortController) { this.__abortController.abort(); delete this.__abortController; + return true; } + return false; }, /** @@ -386,14 +389,20 @@ */ setSrc: function(src, options) { var _this = this; - this.abortLoadingTask(); - this.__abortController = new AbortController(); - var opts = Object.assign({}, options, { signal: this.__abortController.signal }); + if (this.abortLoadingTask()) { + this.fire('loading:aborted'); + } + var abortController = new AbortController(); + this.__abortController = abortController; + var opts = Object.assign({}, options, { signal: abortController.signal }); return fabric.util.loadImage(src, opts).then(function(img) { _this.setElement(img, options); _this._setWidthHeight(); - delete _this.__abortController; return _this; + }).finally(function () { + if (abortController === _this.__abortController) { + delete _this.__abortController; + } }); }, From c9713b14e18aaaba9bfab519c823b60c732fc580 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 19:14:39 +0200 Subject: [PATCH 35/55] Update misc.js --- src/util/misc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/misc.js b/src/util/misc.js index 51c314d5cfa..dc7166231b0 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -515,7 +515,7 @@ else if (signal) { abort = function () { img.src = ''; - reject(new Error('aborted by user')); + reject(new Error('aborted')); }; signal.addEventListener('abort', abort, { once: true }); } From 53b4c5d76ab247a92ae5554c4ca04fc47d83fcd0 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 19:30:15 +0200 Subject: [PATCH 36/55] tests + concurrency test --- test/unit/canvas_static.js | 10 +++++--- test/unit/image.js | 50 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/test/unit/canvas_static.js b/test/unit/canvas_static.js index fd17f180fba..2ae00aae812 100644 --- a/test/unit/canvas_static.js +++ b/test/unit/canvas_static.js @@ -1280,7 +1280,7 @@ QUnit.test('abort loadFromJSON with image background and color', function (assert) { var done = assert.async(); - assert.expect(5); + assert.expect(8); var serialized = JSON.parse(PATH_JSON); serialized.background = 'green'; serialized.backgroundImage = { "type": "image", "originX": "left", "originY": "top", "left": 13.6, "top": -1.4, "width": 3000, "height": 3351, "fill": "rgb(0,0,0)", "stroke": null, "strokeWidth": 0, "strokeDashArray": null, "strokeLineCap": "butt", "strokeDashOffset": 0, "strokeLineJoin": "miter", "strokeMiterLimit": 4, "scaleX": 0.05, "scaleY": 0.05, "angle": 0, "flipX": false, "flipY": false, "opacity": 1, "shadow": null, "visible": true, "backgroundColor": "", "fillRule": "nonzero", "globalCompositeOperation": "source-over", "skewX": 0, "skewY": 0, "src": IMG_SRC, "filters": [], "crossOrigin": "" }; @@ -1289,26 +1289,30 @@ }); canvas.loadFromJSON(serialized).catch(function (err) { assert.equal(err.type, 'abort', 'should be an error object'); + assert.ok(canvas.__abortController instanceof AbortController, 'abort controller of new task should be referenced'); + assert.equal(canvas.__abortController.signal.aborted, false, 'should not be aborted'); }); canvas.loadFromJSON(serialized).then(function () { assert.ok(!canvas.isEmpty(), 'canvas is not empty'); assert.equal(canvas.backgroundColor, 'green'); assert.ok(canvas.backgroundImage instanceof fabric.Image); + assert.equal(canvas.__abortController, undefined, 'abort controller reference should be cleared'); done(); }); }); - QUnit.test('imperative abort loadFromJSON', function (assert) { var done = assert.async(); - assert.expect(3); + assert.expect(5); var serialized = JSON.parse(PATH_JSON); serialized.background = 'green'; serialized.backgroundImage = { "type": "image", "originX": "left", "originY": "top", "left": 13.6, "top": -1.4, "width": 3000, "height": 3351, "fill": "rgb(0,0,0)", "stroke": null, "strokeWidth": 0, "strokeDashArray": null, "strokeLineCap": "butt", "strokeDashOffset": 0, "strokeLineJoin": "miter", "strokeMiterLimit": 4, "scaleX": 0.05, "scaleY": 0.05, "angle": 0, "flipX": false, "flipY": false, "opacity": 1, "shadow": null, "visible": true, "backgroundColor": "", "fillRule": "nonzero", "globalCompositeOperation": "source-over", "skewX": 0, "skewY": 0, "src": IMG_SRC, "filters": [], "crossOrigin": "" }; canvas.loadFromJSON(serialized).catch(function (err) { assert.equal(err.type, 'abort'); }); + assert.ok(typeof canvas.abortLoadingTask === 'function'); assert.ok(canvas.abortLoadingTask(), 'should return true because loading was aborted'); + assert.equal(canvas.__abortController, undefined, 'abort controller reference should be cleared'); assert.ok(canvas.isEmpty(), 'canvas is empty'); done(); }); diff --git a/test/unit/image.js b/test/unit/image.js index 665160d8906..b1327ac912c 100644 --- a/test/unit/image.js +++ b/test/unit/image.js @@ -205,6 +205,56 @@ }); }); + QUnit.test('abort setSrc', function (assert) { + var done = assert.async(); + assert.expect(9); + createImageObject(function (image) { + image.setSrc(IMG_SRC, { crossOrigin: 'anonymous' }).catch(function (err) { + assert.ok(err instanceof Error); + assert.equal(basename(image.getSrc()), basename(IMG_SRC), 'should preserve prev src'); + assert.ok(image.__abortController instanceof AbortController, 'abort controller of new task should be referenced'); + assert.equal(image.__abortController.signal.aborted, false, 'should not be aborted'); + }); + image.setSrc(IMG_SRC, { crossOrigin: 'anonymous' }).then(function () { + assert.equal(image.width, IMG_WIDTH); + assert.equal(image.height, IMG_HEIGHT); + assert.equal(image.getCrossOrigin(), 'anonymous', 'setSrc will respect crossOrigin'); + assert.ok(basename(image.getSrc()), basename(IMG_SRC)); + assert.equal(image.__abortController, undefined, 'abort controller reference should be cleared'); + done(); + }); + }); + }); + + QUnit.test('imperative abort setSrc', function (assert) { + var done = assert.async(); + assert.expect(14); + createImageObject(async function (image) { + assert.ok(typeof image.abortLoadingTask === 'function'); + await image.setSrc(''); + assert.equal(image.getSrc(), ''); + image.setSrc(IMG_SRC, { crossOrigin: 'anonymous' }).catch(function (err) { + assert.ok(err instanceof Error); + assert.equal(image.getSrc(), '', 'should preserve prev src'); + }); + assert.ok(image.abortLoadingTask(), 'should return true because loading was aborted'); + assert.equal(image.__abortController, undefined, 'abort controller reference should be cleared'); + assert.equal(image.getSrc(), ''); + await image.setSrc(IMG_SRC); + var el = image.getElement(); + assert.equal(basename(image.getSrc()), basename(IMG_SRC)); + image.setSrc(IMG_URL_NON_EXISTING).catch(function (err) { + assert.ok(err instanceof Error); + assert.equal(basename(image.getSrc()), basename(IMG_SRC), 'should preserve prev src'); + assert.equal(image.getElement(), el, 'should preserve image element'); + }); + assert.ok(image.abortLoadingTask(), 'should return true because loading was aborted'); + assert.equal(image.__abortController, undefined, 'abort controller reference should be cleared'); + assert.equal(image.getElement(), el); + done(); + }); + }); + QUnit.test('toObject with no element', function(assert) { var done = assert.async(); createImageObject(function(image) { From 2e4b7d4c11a0bafbd254631f7b44cf1f8f3f9f7c Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 19:40:18 +0200 Subject: [PATCH 37/55] removeListener on error --- src/util/dom_request.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/util/dom_request.js b/src/util/dom_request.js index 798025e92c3..3736eeff591 100644 --- a/src/util/dom_request.js +++ b/src/util/dom_request.js @@ -29,6 +29,10 @@ signal = options.signal, abort = abort = function () { xhr.abort(); + }, + removeListener = function () { + signal && signal.removeEventListener('abort', abort); + xhr.onerror = xhr.ontimeout = emptyFn; }; if (signal && signal.aborted) { @@ -40,13 +44,15 @@ /** @ignore */ xhr.onreadystatechange = function() { - if (xhr.readyState === 4) { - signal && signal.removeEventListener('abort', abort); + if (xhr.readyState === XMLHttpRequest.DONE) { + removeListener(); onComplete(xhr); xhr.onreadystatechange = emptyFn; } }; + xhr.onerror = xhr.ontimeout = removeListener; + if (method === 'GET') { body = null; if (typeof options.parameters === 'string') { From 1081460117864167eb1807fc56f77f0c7d4d954c Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 20:30:19 +0200 Subject: [PATCH 38/55] Update misc.js --- src/util/misc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/misc.js b/src/util/misc.js index dc7166231b0..879052ce6e3 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -576,7 +576,7 @@ .finally(function () { signal && signal.removeEventListener('abort', reject); }); - }); + }); }, /** From 8c4cd91069c841e0d077aaab51e4885a59484832 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 20:33:51 +0200 Subject: [PATCH 39/55] bump node version https://nodejs.org/api/globals.html#class-abortcontroller --- .github/workflows/node-unit-tests.yml | 2 +- .github/workflows/visual-node.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/node-unit-tests.yml b/.github/workflows/node-unit-tests.yml index 95fe55e0d71..c525f502fca 100644 --- a/.github/workflows/node-unit-tests.yml +++ b/.github/workflows/node-unit-tests.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [14.17.0, 16.x] + node-version: [15.4.0, 16.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/visual-node.yml b/.github/workflows/visual-node.yml index d8add20b07b..915253f4414 100644 --- a/.github/workflows/visual-node.yml +++ b/.github/workflows/visual-node.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [14.17.0, 16.x] + node-version: [15.4.0, 16.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - uses: actions/checkout@v2 From 3d382540fb2227b84bc2169dc4e418c0805e4ea0 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 23:46:26 +0200 Subject: [PATCH 40/55] Update dom_request.js --- src/util/dom_request.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/dom_request.js b/src/util/dom_request.js index 3736eeff591..3b8c84acef1 100644 --- a/src/util/dom_request.js +++ b/src/util/dom_request.js @@ -44,7 +44,7 @@ /** @ignore */ xhr.onreadystatechange = function() { - if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.readyState === 4) { removeListener(); onComplete(xhr); xhr.onreadystatechange = emptyFn; From a3030dca577b604ca1fa127db3595c62c0ab2cea Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 24 Mar 2022 23:57:53 +0200 Subject: [PATCH 41/55] bump node version 8c4cd910 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5d0122582ce..1b6a8b3bd5d 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "uglify-js": "3.3.x" }, "engines": { - "node": ">=14.17.0" + "node": ">=15.4.0" }, "main": "./dist/fabric.js", "dependencies": {} From 19b5ea52e7c54d6e8052229cb284af3c33c05025 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Fri, 25 Mar 2022 13:57:01 +0300 Subject: [PATCH 42/55] Update image.class.js --- src/shapes/image.class.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/shapes/image.class.js b/src/shapes/image.class.js index 8e080ab53ae..e15cc698ee2 100644 --- a/src/shapes/image.class.js +++ b/src/shapes/image.class.js @@ -17,6 +17,7 @@ * Image class * @class fabric.Image * @extends fabric.Object + * @fires loading:aborted * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#images} * @see {@link fabric.Image#initialize} for constructor definition */ From 13c6940083d3b05bbe3eab128dd9cd85407421b4 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Fri, 25 Mar 2022 14:00:05 +0300 Subject: [PATCH 43/55] Update image.js --- test/unit/image.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/unit/image.js b/test/unit/image.js index b1327ac912c..b23e0a2707a 100644 --- a/test/unit/image.js +++ b/test/unit/image.js @@ -206,9 +206,12 @@ }); QUnit.test('abort setSrc', function (assert) { - var done = assert.async(); - assert.expect(9); + var done = assert.async(), called = 0; + assert.expect(10); createImageObject(function (image) { + image.on('loading:aborted', function () { + called++; + }); image.setSrc(IMG_SRC, { crossOrigin: 'anonymous' }).catch(function (err) { assert.ok(err instanceof Error); assert.equal(basename(image.getSrc()), basename(IMG_SRC), 'should preserve prev src'); @@ -221,6 +224,7 @@ assert.equal(image.getCrossOrigin(), 'anonymous', 'setSrc will respect crossOrigin'); assert.ok(basename(image.getSrc()), basename(IMG_SRC)); assert.equal(image.__abortController, undefined, 'abort controller reference should be cleared'); + assert.equal(called, 1, 'should have fired `loading:aborted` event once'); done(); }); }); From 639223e413c537146ea0df3ef308bc5ea1444343 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sat, 26 Mar 2022 02:09:59 +0300 Subject: [PATCH 44/55] bump node to v16 --- .github/workflows/node-unit-tests.yml | 2 +- .github/workflows/visual-node.yml | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/node-unit-tests.yml b/.github/workflows/node-unit-tests.yml index c525f502fca..d96a2555370 100644 --- a/.github/workflows/node-unit-tests.yml +++ b/.github/workflows/node-unit-tests.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [15.4.0, 16.x] + node-version: [16.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/visual-node.yml b/.github/workflows/visual-node.yml index 915253f4414..cd0c7bb8c14 100644 --- a/.github/workflows/visual-node.yml +++ b/.github/workflows/visual-node.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [15.4.0, 16.x] + node-version: [16.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - uses: actions/checkout@v2 diff --git a/package.json b/package.json index 1b6a8b3bd5d..b37038c0226 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "uglify-js": "3.3.x" }, "engines": { - "node": ">=15.4.0" + "node": ">=16.0.0" }, "main": "./dist/fabric.js", "dependencies": {} From 54e1c0e2adc3ac00c9bb53a82b04cf3693b670db Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sun, 26 Jun 2022 06:51:32 +0300 Subject: [PATCH 45/55] typo --- src/filters/blendimage_filter.class.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/filters/blendimage_filter.class.js b/src/filters/blendimage_filter.class.js index f6032d56705..82ef7533ca5 100644 --- a/src/filters/blendimage_filter.class.js +++ b/src/filters/blendimage_filter.class.js @@ -232,7 +232,7 @@ /** * Create filter instance from an object representation * @static - * @param {oject} object Object to create an instance from + * @param {object} object Object to create an instance from * @param {object} [options] * @param {AbortSignal} [options.signal] handle aborting image loading, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal * @returns {Promise} From fd6573ecbbf4f79be5b206e56213fed62476a9bd Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sun, 26 Jun 2022 06:55:01 +0300 Subject: [PATCH 46/55] rename --- src/shapes/image.class.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/shapes/image.class.js b/src/shapes/image.class.js index eff62862d06..fb25695b0a1 100644 --- a/src/shapes/image.class.js +++ b/src/shapes/image.class.js @@ -732,14 +732,14 @@ * Creates an instance of fabric.Image from an URL string * @static * @param {String} url URL to create an image from - * @param {object} [imgOptions] Options object + * @param {object} [options] Options object * @param {string} [options.crossOrigin] cors value for the image loading, default to anonymous * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal * @returns {Promise} */ - fabric.Image.fromURL = function(url, imgOptions) { - return fabric.util.loadImage(url, imgOptions || {}).then(function(img) { - return new fabric.Image(img, imgOptions); + fabric.Image.fromURL = function(url, options) { + return fabric.util.loadImage(url, options || {}).then(function(img) { + return new fabric.Image(img, options); }); }; From 1ff31f137f73988930d0dfe38b6fb617863b3316 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sun, 26 Jun 2022 06:56:14 +0300 Subject: [PATCH 47/55] rename --- src/shapes/image.class.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/shapes/image.class.js b/src/shapes/image.class.js index fb25695b0a1..ac18c102f54 100644 --- a/src/shapes/image.class.js +++ b/src/shapes/image.class.js @@ -706,25 +706,25 @@ * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal * @returns {Promise} */ - fabric.Image.fromObject = function (_object, options) { - var object = Object.assign({}, _object), - filters = object.filters, - resizeFilter = object.resizeFilter; + fabric.Image.fromObject = function (object, options) { + var _object = Object.assign({}, object), + filters = _object.filters, + resizeFilter = _object.resizeFilter; // the generic enliving will fail on filters for now - delete object.resizeFilter; - delete object.filters; + delete _object.resizeFilter; + delete _object.filters; var imageOptions = Object.assign({}, options, { crossOrigin: _object.crossOrigin }), filterOptions = Object.assign({}, options, { namespace: fabric.Image.filters }); return Promise.all([ - fabric.util.loadImage(object.src, imageOptions), + fabric.util.loadImage(_object.src, imageOptions), filters && fabric.util.enlivenObjects(filters, filterOptions), resizeFilter && fabric.util.enlivenObjects([resizeFilter], filterOptions), - fabric.util.enlivenObjectEnlivables(object, options), + fabric.util.enlivenObjectEnlivables(_object, options), ]) .then(function(imgAndFilters) { - object.filters = imgAndFilters[1] || []; - object.resizeFilter = imgAndFilters[2] && imgAndFilters[2][0]; - return new fabric.Image(imgAndFilters[0], Object.assign(object, imgAndFilters[3])); + _object.filters = imgAndFilters[1] || []; + _object.resizeFilter = imgAndFilters[2] && imgAndFilters[2][0]; + return new fabric.Image(imgAndFilters[0], Object.assign(_object, imgAndFilters[3])); }); }; From 4f5b6e2418c9950b5690de2df4ebc4e47708c3cb Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 28 Jun 2022 12:45:39 +0300 Subject: [PATCH 48/55] incoming signal **ONLY** + revert node to v14 --- .eslintrc.json | 3 +-- .github/workflows/node-unit-tests.yml | 2 +- .github/workflows/visual-node.yml | 2 +- src/mixins/canvas_serialization.mixin.js | 32 ++++-------------------- src/shapes/image.class.js | 30 +++------------------- src/static_canvas.class.js | 1 - 6 files changed, 12 insertions(+), 58 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 4e8cd083eb2..f3d848c6348 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -12,8 +12,7 @@ "Buffer": true, "process": true, "QUnit": true, - "assert": true, - "AbortController": true + "assert": true }, "rules": { "semi": 2, diff --git a/.github/workflows/node-unit-tests.yml b/.github/workflows/node-unit-tests.yml index 97ac2707780..22755859711 100644 --- a/.github/workflows/node-unit-tests.yml +++ b/.github/workflows/node-unit-tests.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [16.x] + node-version: [14.x, 16.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/visual-node.yml b/.github/workflows/visual-node.yml index 60de23a8c81..edf20cb0d90 100644 --- a/.github/workflows/visual-node.yml +++ b/.github/workflows/visual-node.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [16.x] + node-version: [14.x, 16.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - uses: actions/checkout@v2 diff --git a/src/mixins/canvas_serialization.mixin.js b/src/mixins/canvas_serialization.mixin.js index 539eacbded3..e8682759d3c 100644 --- a/src/mixins/canvas_serialization.mixin.js +++ b/src/mixins/canvas_serialization.mixin.js @@ -1,23 +1,12 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { - /** - * Aborts instance's loading task ({@link fabric.Canvas#loadFromJSON} etc.) if exists - * @returns {boolean} true if aborted - */ - abortLoadingTask: function () { - if (this.__abortController) { - this.__abortController.abort(); - delete this.__abortController; - return true; - } - return false; - }, - /** * Populates canvas with data from the specified JSON. * JSON format must conform to the one of {@link fabric.Canvas#toJSON} * @param {String|Object} json JSON string or object * @param {Function} [reviver] Method for further parsing of JSON elements, called after each fabric object created. + * @param {Object} [options] options + * @param {AbortSignal} [options.signal] see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal * @return {Promise} instance * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#deserialization} * @see {@link http://jsfiddle.net/fabricjs/fmgXt/|jsFiddle demo} @@ -32,10 +21,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * ... canvas is restored, add your code. * }); */ - loadFromJSON: function (json, reviver) { - if (this.abortLoadingTask()) { - this.fire('loading:aborted', { from: 'json' }); - } + loadFromJSON: function (json, reviver, options) { if (!json) { return Promise.reject(new Error('fabric.js: `json` is undefined')); } @@ -45,15 +31,11 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati ? JSON.parse(json) : Object.assign({}, json); - var _this = this, - renderOnAddRemove = this.renderOnAddRemove, - abortController = new AbortController(); - - this.__abortController = abortController; + var _this = this, renderOnAddRemove = this.renderOnAddRemove; this.renderOnAddRemove = false; return Promise.all([ - fabric.util.enlivenObjects(serialized.objects || [], { reviver: reviver, signal: abortController.signal }), + fabric.util.enlivenObjects(serialized.objects || [], Object.assign({ reviver: reviver }, options)), fabric.util.enlivenObjectEnlivables({ backgroundImage: serialized.backgroundImage, backgroundColor: serialized.background, @@ -69,10 +51,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati _this.renderOnAddRemove = renderOnAddRemove; _this.set(enlivedMap); return _this; - }).finally(function () { - if (abortController === _this.__abortController) { - delete _this.__abortController; - } }); }, diff --git a/src/shapes/image.class.js b/src/shapes/image.class.js index ac18c102f54..7e3bd4ffb16 100644 --- a/src/shapes/image.class.js +++ b/src/shapes/image.class.js @@ -17,7 +17,6 @@ * Image class * @class fabric.Image * @extends fabric.Object - * @fires loading:aborted * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#images} * @see {@link fabric.Image#initialize} for constructor definition */ @@ -365,42 +364,21 @@ }, /** - * Aborts pending image loading task - * @returns {boolean} true if aborted - */ - abortLoadingTask: function () { - if (this.__abortController) { - this.__abortController.abort(); - delete this.__abortController; - return true; - } - return false; - }, - - /** - * Sets source of an image + * Loads and sets source of an image\ + * **IMPORTANT**: It is recommended to abort loading tasks before calling this method to prevent race conditions and unnecessary networking * @param {String} src Source string (URL) * @param {Object} [options] Options object + * @param {AbortSignal} [options.signal] see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal * @param {String} [options.crossOrigin] crossOrigin value (one of "", "anonymous", "use-credentials") * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes * @return {Promise} thisArg */ setSrc: function(src, options) { var _this = this; - if (this.abortLoadingTask()) { - this.fire('loading:aborted'); - } - var abortController = new AbortController(); - this.__abortController = abortController; - var opts = Object.assign({}, options, { signal: abortController.signal }); - return fabric.util.loadImage(src, opts).then(function(img) { + return fabric.util.loadImage(src, options).then(function (img) { _this.setElement(img, options); _this._setWidthHeight(); return _this; - }).finally(function () { - if (abortController === _this.__abortController) { - delete _this.__abortController; - } }); }, diff --git a/src/static_canvas.class.js b/src/static_canvas.class.js index 38cd9f63c66..82154c11730 100644 --- a/src/static_canvas.class.js +++ b/src/static_canvas.class.js @@ -31,7 +31,6 @@ * @fires canvas:cleared * @fires object:added * @fires object:removed - * @fires loading:aborted */ // eslint-disable-next-line max-len fabric.StaticCanvas = fabric.util.createClass(fabric.CommonMethods, fabric.Collection, /** @lends fabric.StaticCanvas.prototype */ { From 52ba6fbcb734633222cefffcd0f42438ce132a9e Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 28 Jun 2022 12:48:22 +0300 Subject: [PATCH 49/55] missed out --- src/mixins/canvas_serialization.mixin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mixins/canvas_serialization.mixin.js b/src/mixins/canvas_serialization.mixin.js index e8682759d3c..521a26f2bb3 100644 --- a/src/mixins/canvas_serialization.mixin.js +++ b/src/mixins/canvas_serialization.mixin.js @@ -42,7 +42,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati overlayImage: serialized.overlayImage, overlayColor: serialized.overlay, clipPath: serialized.clipPath, - }, { signal: abortController.signal }) + }, { signal: options && options.signal }) ]) .then(function (res) { var enlived = res[0], enlivedMap = res[1]; From ec86828c378bcda2c98cbab836989d6193580998 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 28 Jun 2022 12:58:12 +0300 Subject: [PATCH 50/55] tests --- test/unit/image.js | 54 ---------------------------------------------- test/unit/util.js | 10 +++++++++ 2 files changed, 10 insertions(+), 54 deletions(-) diff --git a/test/unit/image.js b/test/unit/image.js index b23e0a2707a..665160d8906 100644 --- a/test/unit/image.js +++ b/test/unit/image.js @@ -205,60 +205,6 @@ }); }); - QUnit.test('abort setSrc', function (assert) { - var done = assert.async(), called = 0; - assert.expect(10); - createImageObject(function (image) { - image.on('loading:aborted', function () { - called++; - }); - image.setSrc(IMG_SRC, { crossOrigin: 'anonymous' }).catch(function (err) { - assert.ok(err instanceof Error); - assert.equal(basename(image.getSrc()), basename(IMG_SRC), 'should preserve prev src'); - assert.ok(image.__abortController instanceof AbortController, 'abort controller of new task should be referenced'); - assert.equal(image.__abortController.signal.aborted, false, 'should not be aborted'); - }); - image.setSrc(IMG_SRC, { crossOrigin: 'anonymous' }).then(function () { - assert.equal(image.width, IMG_WIDTH); - assert.equal(image.height, IMG_HEIGHT); - assert.equal(image.getCrossOrigin(), 'anonymous', 'setSrc will respect crossOrigin'); - assert.ok(basename(image.getSrc()), basename(IMG_SRC)); - assert.equal(image.__abortController, undefined, 'abort controller reference should be cleared'); - assert.equal(called, 1, 'should have fired `loading:aborted` event once'); - done(); - }); - }); - }); - - QUnit.test('imperative abort setSrc', function (assert) { - var done = assert.async(); - assert.expect(14); - createImageObject(async function (image) { - assert.ok(typeof image.abortLoadingTask === 'function'); - await image.setSrc(''); - assert.equal(image.getSrc(), ''); - image.setSrc(IMG_SRC, { crossOrigin: 'anonymous' }).catch(function (err) { - assert.ok(err instanceof Error); - assert.equal(image.getSrc(), '', 'should preserve prev src'); - }); - assert.ok(image.abortLoadingTask(), 'should return true because loading was aborted'); - assert.equal(image.__abortController, undefined, 'abort controller reference should be cleared'); - assert.equal(image.getSrc(), ''); - await image.setSrc(IMG_SRC); - var el = image.getElement(); - assert.equal(basename(image.getSrc()), basename(IMG_SRC)); - image.setSrc(IMG_URL_NON_EXISTING).catch(function (err) { - assert.ok(err instanceof Error); - assert.equal(basename(image.getSrc()), basename(IMG_SRC), 'should preserve prev src'); - assert.equal(image.getElement(), el, 'should preserve image element'); - }); - assert.ok(image.abortLoadingTask(), 'should return true because loading was aborted'); - assert.equal(image.__abortController, undefined, 'abort controller reference should be cleared'); - assert.equal(image.getElement(), el); - done(); - }); - }); - QUnit.test('toObject with no element', function(assert) { var done = assert.async(); createImageObject(function(image) { diff --git a/test/unit/util.js b/test/unit/util.js index ff7f58b9e1f..8c2a6d4ac21 100644 --- a/test/unit/util.js +++ b/test/unit/util.js @@ -512,6 +512,16 @@ }); }); + QUnit.test('fabric.util.loadImage with AbortController', function (assert) { + var done = assert.async(); + var abortController = new AbortController(); + fabric.util.loadImage(IMG_URL, { signal: abortController.signal }).catch(function (err) { + assert.ok(err instanceof Error, 'callback should be invoked with error set to true'); + done(); + }); + abortController.abort(); + }); + var SVG_WITH_1_ELEMENT = '\ \ \ From a61e3e6c589534adcf20fded2cb4b2955f6c1524 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 28 Jun 2022 13:14:44 +0300 Subject: [PATCH 51/55] Update canvas_serialization.mixin.js --- src/mixins/canvas_serialization.mixin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mixins/canvas_serialization.mixin.js b/src/mixins/canvas_serialization.mixin.js index 521a26f2bb3..d81c1153ebe 100644 --- a/src/mixins/canvas_serialization.mixin.js +++ b/src/mixins/canvas_serialization.mixin.js @@ -35,7 +35,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati this.renderOnAddRemove = false; return Promise.all([ - fabric.util.enlivenObjects(serialized.objects || [], Object.assign({ reviver: reviver }, options)), + fabric.util.enlivenObjects(serialized.objects || [], { reviver: reviver, signal: options && options.signal }), fabric.util.enlivenObjectEnlivables({ backgroundImage: serialized.backgroundImage, backgroundColor: serialized.background, From b15bea1c9379f4717e3a7c787a4acf38dbb94683 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 28 Jun 2022 13:15:17 +0300 Subject: [PATCH 52/55] conform rejection object --- src/util/misc.js | 4 ++-- test/unit/canvas_static.js | 42 ++++++++------------------------------ test/unit/util.js | 10 ++++----- 3 files changed, 16 insertions(+), 40 deletions(-) diff --git a/src/util/misc.js b/src/util/misc.js index d448f88d484..721ed082963 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -513,9 +513,9 @@ return reject(new Error('`options.signal` is in `aborted` state')); } else if (signal) { - abort = function () { + abort = function (err) { img.src = ''; - reject(new Error('aborted')); + reject(err); }; signal.addEventListener('abort', abort, { once: true }); } diff --git a/test/unit/canvas_static.js b/test/unit/canvas_static.js index 00b9b4b1139..3b4265abee6 100644 --- a/test/unit/canvas_static.js +++ b/test/unit/canvas_static.js @@ -1328,43 +1328,19 @@ }); }); - QUnit.test('abort loadFromJSON with image background and color', function (assert) { + QUnit.test('loadFromJSON with AbortController', function (assert) { var done = assert.async(); - assert.expect(8); + assert.expect(1); var serialized = JSON.parse(PATH_JSON); serialized.background = 'green'; serialized.backgroundImage = { "type": "image", "originX": "left", "originY": "top", "left": 13.6, "top": -1.4, "width": 3000, "height": 3351, "fill": "rgb(0,0,0)", "stroke": null, "strokeWidth": 0, "strokeDashArray": null, "strokeLineCap": "butt", "strokeDashOffset": 0, "strokeLineJoin": "miter", "strokeMiterLimit": 4, "scaleX": 0.05, "scaleY": 0.05, "angle": 0, "flipX": false, "flipY": false, "opacity": 1, "shadow": null, "visible": true, "backgroundColor": "", "fillRule": "nonzero", "globalCompositeOperation": "source-over", "skewX": 0, "skewY": 0, "src": IMG_SRC, "filters": [], "crossOrigin": "" }; - canvas.on('loading:aborted', function (opt) { - assert.equal(opt.from, 'json'); - }); - canvas.loadFromJSON(serialized).catch(function (err) { - assert.equal(err.type, 'abort', 'should be an error object'); - assert.ok(canvas.__abortController instanceof AbortController, 'abort controller of new task should be referenced'); - assert.equal(canvas.__abortController.signal.aborted, false, 'should not be aborted'); - }); - canvas.loadFromJSON(serialized).then(function () { - assert.ok(!canvas.isEmpty(), 'canvas is not empty'); - assert.equal(canvas.backgroundColor, 'green'); - assert.ok(canvas.backgroundImage instanceof fabric.Image); - assert.equal(canvas.__abortController, undefined, 'abort controller reference should be cleared'); - done(); - }); - }); - - QUnit.test('imperative abort loadFromJSON', function (assert) { - var done = assert.async(); - assert.expect(5); - var serialized = JSON.parse(PATH_JSON); - serialized.background = 'green'; - serialized.backgroundImage = { "type": "image", "originX": "left", "originY": "top", "left": 13.6, "top": -1.4, "width": 3000, "height": 3351, "fill": "rgb(0,0,0)", "stroke": null, "strokeWidth": 0, "strokeDashArray": null, "strokeLineCap": "butt", "strokeDashOffset": 0, "strokeLineJoin": "miter", "strokeMiterLimit": 4, "scaleX": 0.05, "scaleY": 0.05, "angle": 0, "flipX": false, "flipY": false, "opacity": 1, "shadow": null, "visible": true, "backgroundColor": "", "fillRule": "nonzero", "globalCompositeOperation": "source-over", "skewX": 0, "skewY": 0, "src": IMG_SRC, "filters": [], "crossOrigin": "" }; - canvas.loadFromJSON(serialized).catch(function (err) { - assert.equal(err.type, 'abort'); - }); - assert.ok(typeof canvas.abortLoadingTask === 'function'); - assert.ok(canvas.abortLoadingTask(), 'should return true because loading was aborted'); - assert.equal(canvas.__abortController, undefined, 'abort controller reference should be cleared'); - assert.ok(canvas.isEmpty(), 'canvas is empty'); - done(); + var abortController = new AbortController(); + canvas.loadFromJSON(serialized, null, { signal: abortController.signal }) + .catch(function (err) { + assert.equal(err.type, 'abort', 'should be an abort event'); + done(); + }); + abortController.abort(); }); QUnit.test('loadFromJSON custom properties', function(assert) { diff --git a/test/unit/util.js b/test/unit/util.js index 8c2a6d4ac21..a228112dc4c 100644 --- a/test/unit/util.js +++ b/test/unit/util.js @@ -503,7 +503,6 @@ } }); - QUnit.test('fabric.util.loadImage with url for a non exsiting image', function(assert) { var done = assert.async(); fabric.util.loadImage(IMG_URL_NON_EXISTING).catch(function(err) { @@ -515,10 +514,11 @@ QUnit.test('fabric.util.loadImage with AbortController', function (assert) { var done = assert.async(); var abortController = new AbortController(); - fabric.util.loadImage(IMG_URL, { signal: abortController.signal }).catch(function (err) { - assert.ok(err instanceof Error, 'callback should be invoked with error set to true'); - done(); - }); + fabric.util.loadImage(IMG_URL, { signal: abortController.signal }) + .catch(function (err) { + assert.equal(err.type, 'abort', 'should be an abort event'); + done(); + }); abortController.abort(); }); From db16268d8d5570e9c28bf4ce2ff3e4b3cb3c478e Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 28 Jun 2022 13:18:54 +0300 Subject: [PATCH 53/55] Update canvas_serialization.mixin.js --- src/mixins/canvas_serialization.mixin.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/mixins/canvas_serialization.mixin.js b/src/mixins/canvas_serialization.mixin.js index d81c1153ebe..0709a529a55 100644 --- a/src/mixins/canvas_serialization.mixin.js +++ b/src/mixins/canvas_serialization.mixin.js @@ -3,6 +3,9 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Populates canvas with data from the specified JSON. * JSON format must conform to the one of {@link fabric.Canvas#toJSON} + * + * **IMPORTANT**: It is recommended to abort loading tasks before calling this method to prevent race conditions and unnecessary networking + * * @param {String|Object} json JSON string or object * @param {Function} [reviver] Method for further parsing of JSON elements, called after each fabric object created. * @param {Object} [options] options @@ -20,6 +23,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * }).then((canvas) => { * ... canvas is restored, add your code. * }); + * */ loadFromJSON: function (json, reviver, options) { if (!json) { From efaf0b42eb98ca695986320c6ce914ec046cf81d Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 28 Jun 2022 13:23:50 +0300 Subject: [PATCH 54/55] restore config for tests --- .eslintrc.json | 3 ++- .github/workflows/node-unit-tests.yml | 2 +- .github/workflows/visual-node.yml | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index f3d848c6348..4e8cd083eb2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -12,7 +12,8 @@ "Buffer": true, "process": true, "QUnit": true, - "assert": true + "assert": true, + "AbortController": true }, "rules": { "semi": 2, diff --git a/.github/workflows/node-unit-tests.yml b/.github/workflows/node-unit-tests.yml index 22755859711..97ac2707780 100644 --- a/.github/workflows/node-unit-tests.yml +++ b/.github/workflows/node-unit-tests.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [14.x, 16.x] + node-version: [16.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/visual-node.yml b/.github/workflows/visual-node.yml index edf20cb0d90..60de23a8c81 100644 --- a/.github/workflows/visual-node.yml +++ b/.github/workflows/visual-node.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [14.x, 16.x] + node-version: [16.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - uses: actions/checkout@v2 From 06a1d6250893e04c5312e6560b5170d8713c9316 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 28 Jun 2022 13:26:04 +0300 Subject: [PATCH 55/55] revert node v --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0f508f0acca..587e01acdbe 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "uglify-js": "^3.15.5" }, "engines": { - "node": ">=16.0.0" + "node": ">=14.0.0" }, "main": "./dist/fabric.js" }