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 diff --git a/src/filters/blendimage_filter.class.js b/src/filters/blendimage_filter.class.js index 8567c2deb02..82ef7533ca5 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 {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} */ - 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) { return new fabric.Image.filters.BlendImage(Object.assign({}, object, { image: image })); }); }; 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 }); }); diff --git a/src/mixins/canvas_serialization.mixin.js b/src/mixins/canvas_serialization.mixin.js index e3496d51d73..0709a529a55 100644 --- a/src/mixins/canvas_serialization.mixin.js +++ b/src/mixins/canvas_serialization.mixin.js @@ -1,11 +1,16 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { + /** * 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 + * @param {AbortSignal} [options.signal] see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal * @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 @@ -18,10 +23,11 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * }).then((canvas) => { * ... canvas is restored, add your code. * }); + * */ - loadFromJSON: function (json, reviver) { + loadFromJSON: function (json, reviver, options) { if (!json) { - return; + return Promise.reject(new Error('fabric.js: `json` is undefined')); } // serialize if it wasn't already @@ -29,26 +35,26 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati ? JSON.parse(json) : Object.assign({}, json); - var _this = this, - renderOnAddRemove = this.renderOnAddRemove; - + var _this = this, renderOnAddRemove = this.renderOnAddRemove; this.renderOnAddRemove = false; - return fabric.util.enlivenObjects(serialized.objects || [], '', reviver) - .then(function(enlived) { + return Promise.all([ + fabric.util.enlivenObjects(serialized.objects || [], { reviver: reviver, signal: options && options.signal }), + fabric.util.enlivenObjectEnlivables({ + backgroundImage: serialized.backgroundImage, + backgroundColor: serialized.background, + overlayImage: serialized.overlayImage, + overlayColor: serialized.overlay, + clipPath: serialized.clipPath, + }, { signal: options && options.signal }) + ]) + .then(function (res) { + var enlived = res[0], enlivedMap = res[1]; _this.clear(); - return fabric.util.enlivenObjectEnlivables({ - backgroundImage: serialized.backgroundImage, - backgroundColor: serialized.background, - overlayImage: serialized.overlayImage, - overlayColor: serialized.overlay, - clipPath: serialized.clipPath, - }) - .then(function(enlivedMap) { - _this.__setupCanvas(serialized, enlived, renderOnAddRemove); - _this.set(enlivedMap); - return _this; - }); + _this.__setupCanvas(serialized, enlived); + _this.renderOnAddRemove = renderOnAddRemove; + _this.set(enlivedMap); + return _this; }); }, @@ -56,16 +62,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; diff --git a/src/parser.js b/src/parser.js index ebb9bdba90a..585c5558767 100644 --- a/src/parser.js +++ b/src/parser.js @@ -686,12 +686,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 Error('`options.signal` is in `aborted` state'); + } parseUseDirectives(doc); var svgUid = fabric.Object.__uid++, i, len, @@ -699,6 +702,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("*") @@ -1044,13 +1048,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, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal */ 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 && options.signal }); function onComplete(r) { @@ -1075,6 +1081,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, 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 4a6f6aa94e4..6e35049b099 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] 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({}, options, { crossOrigin: object.crossOrigin }); + return fabric.util.loadImage(object.source, imageOptions) .then(function(img) { patternOptions.source = img; return new fabric.Pattern(patternOptions); diff --git a/src/shapes/image.class.js b/src/shapes/image.class.js index 8b77e28b75c..7e3bd4ffb16 100644 --- a/src/shapes/image.class.js +++ b/src/shapes/image.class.js @@ -364,16 +364,18 @@ }, /** - * 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; - return fabric.util.loadImage(src, options).then(function(img) { + return fabric.util.loadImage(src, options).then(function (img) { _this.setElement(img, options); _this._setWidthHeight(); return _this; @@ -678,25 +680,29 @@ * 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] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal * @returns {Promise} */ - fabric.Image.fromObject = function(_object) { - 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, { crossOrigin: _object.crossOrigin }), - filters && fabric.util.enlivenObjects(filters, 'fabric.Image.filters'), - resizeFilter && fabric.util.enlivenObjects([resizeFilter], 'fabric.Image.filters'), - fabric.util.enlivenObjectEnlivables(object), + fabric.util.loadImage(_object.src, imageOptions), + filters && fabric.util.enlivenObjects(filters, filterOptions), + resizeFilter && fabric.util.enlivenObjects([resizeFilter], filterOptions), + 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])); }); }; @@ -704,12 +710,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); }); }; @@ -729,6 +737,7 @@ * @static * @param {SVGElement} element Element to parse * @param {Object} [options] Options object + * @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/itext.class.js b/src/shapes/itext.class.js index 8040e65f15b..596865f98a7 100644 --- a/src/shapes/itext.class.js +++ b/src/shapes/itext.class.js @@ -531,6 +531,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 158465fb504..92194be5146 100644 --- a/src/shapes/object.class.js +++ b/src/shapes/object.class.js @@ -1928,23 +1928,33 @@ 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 {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) { + fabric.Object._fromObject = function(klass, object, 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); + return options && options.extraParam ? new klass(object[options.extraParam], newObject) : new klass(newObject); }); }; - fabric.Object.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Object, object); + /** + * + * @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, 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 fbf9301cd5b..7c76feb4116 100644 --- a/src/shapes/polyline.class.js +++ b/src/shapes/polyline.class.js @@ -263,7 +263,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 2ea9cbbe867..8fd8cacc57d 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -1730,7 +1730,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 3faff27b7b0..648c9eab7d7 100644 --- a/src/shapes/textbox.class.js +++ b/src/shapes/textbox.class.js @@ -472,6 +472,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); diff --git a/src/util/dom_request.js b/src/util/dom_request.js index 03a82ab7d42..3b8c84acef1 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] 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 */ @@ -22,18 +23,36 @@ 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(); + }, + removeListener = function () { + signal && signal.removeEventListener('abort', abort); + xhr.onerror = xhr.ontimeout = emptyFn; + }; + + if (signal && signal.aborted) { + throw new Error('`options.signal` is in `aborted` state'); + } + else if (signal) { + signal.addEventListener('abort', abort, { once: true }); + } /** @ignore */ xhr.onreadystatechange = function() { if (xhr.readyState === 4) { + removeListener(); onComplete(xhr); xhr.onreadystatechange = emptyFn; } }; + xhr.onerror = xhr.ontimeout = removeListener; + if (method === 'GET') { body = null; if (typeof options.parameters === 'string') { diff --git a/src/util/misc.js b/src/util/misc.js index 3bcf6f79ca0..721ed082963 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, @@ -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,41 +497,32 @@ 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 * @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] handle aborting, 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 abort, signal = options && options.signal; + return new Promise(function (resolve, reject) { + if (signal && signal.aborted) { + return reject(new Error('`options.signal` is in `aborted` state')); + } + else if (signal) { + abort = function (err) { + img.src = ''; + reject(err); + }; + signal.addEventListener('abort', abort, { once: true }); + } var img = fabric.util.createImage(); var done = function() { img.onload = img.onerror = null; + signal && abort && signal.removeEventListener('abort', abort); resolve(img); }; if (!url) { @@ -540,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); @@ -553,51 +545,94 @@ * @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] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal + * @returns {Promise} */ - enlivenObjects: function(objects, namespace, reviver) { - 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); - return fabricInstance; - }); - })); + enlivenObjects: function(objects, options) { + options = options || {}; + 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); + }); + }); }, /** * 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] 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) { - // enlive every possible property - var promises = Object.values(serializedObject).map(function(value) { - if (!value) { + enlivenObjectEnlivables: function (serializedObject, options) { + 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]).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); - } - 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; - }, {}); }); }, diff --git a/test/unit/canvas_static.js b/test/unit/canvas_static.js index 99166bedb27..3b4265abee6 100644 --- a/test/unit/canvas_static.js +++ b/test/unit/canvas_static.js @@ -1328,6 +1328,21 @@ }); }); + QUnit.test('loadFromJSON with AbortController', function (assert) { + var done = assert.async(); + 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": "" }; + 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) { var done = assert.async(); var rect = new fabric.Rect({ width: 10, height: 20 }); diff --git a/test/unit/util.js b/test/unit/util.js index fbcb9011c53..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) { @@ -512,6 +511,17 @@ }); }); + 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.equal(err.type, 'abort', 'should be an abort event'); + done(); + }); + abortController.abort(); + }); + var SVG_WITH_1_ELEMENT = '\ \ \ @@ -705,12 +715,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'} };