diff --git a/README.md b/README.md index 6690424..cf791a3 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ It supports RTL layout and dark mode out of the box. ⚠️ **v2 is still in BETA stage** ⚠️ -![v2 Beta16](https://img.shields.io/badge/v2_Beta16-2024/05/30-green?style=plastic) +![v2 Beta17](https://img.shields.io/badge/v2_Beta17-2024/06/13-green?style=plastic) **Apologies in advance for any problem or bug you face with this module.** **Please report any problem or bug you face so it can be fixed.** @@ -35,9 +35,10 @@ It supports RTL layout and dark mode out of the box. #### Version 2 - [![MohsinAli](https://img.shields.io/badge/MohsinAli-Debug_%7C_Test_%7C_Fix-red?style=plastic)](https://github.com/mohsinalimat) - [![Robert C](https://img.shields.io/badge/Robert_C-Debug_%7C_Test-blue?style=plastic)](https://github.com/robert1112) -- [![NirajRegmi](https://img.shields.io/badge/NirajRegmi-Debug_%7C_Test-orange?style=plastic)](https://github.com/NirajRegmi) +- [![NirajRegmi](https://img.shields.io/badge/NirajRegmi-Debug_%7C_Test-blue?style=plastic)](https://github.com/NirajRegmi) +- [![galaxlabs](https://img.shields.io/badge/galaxlabs-Enhancement-a2eeef?style=plastic)](https://github.com/galaxlabs) #### Version 1 -- [![CA. B.C.Chechani](https://img.shields.io/badge/CA._B.C.Chechani-Debug_%7C_Test-green?style=plastic)](https://github.com/chechani) +- [![CA. B.C.Chechani](https://img.shields.io/badge/CA._B.C.Chechani-Debug_%7C_Test-blue?style=plastic)](https://github.com/chechani) --- @@ -179,7 +180,7 @@ You can't modify the original fields of a doctype, so create a new field or clon | :--- | :--- | | **dialog_title** | Upload dialog title to be displayed ️(🔶Frappe >= v14.0.0).

🔹Example: **"Upload Images"**
🔹Default: **"Upload"** | | **upload_notes** | Upload text to be displayed.

🔹Example: **"Only images and videos, with maximum size of 2MB, are allowed to be uploaded"**
🔹Default: **""** | -| **disable_auto_save** 🔴 | Disable form auto save after upload.

🔹Default: **false** | +| **disable_auto_save** | Disable form auto save after upload.

🔹Default: **false** | | **disable_file_browser** | Disable file browser uploads.

⚠️ *(File browser is always disabled in Web Form)*

🔹Default: **false** | | **allow_multiple** | Allow multiple uploads.

⚠️ *(Field value is a JSON array of files url)*

🔹Default: **false** | | **max_file_size** | Maximum file size (in bytes) that is allowed to be uploaded.

🔹Example: **2048** for **2KB**
🔹Default: **Value of maximum file size in Frappe's settings** | @@ -190,17 +191,23 @@ You can't modify the original fields of a doctype, so create a new field or clon | **allowed_filename** | Only allow files that match a specific file name to be uploaded.

🔹Example: (String)**"picture.png"** or (RegExp String)**"/picture\-([0-9]+)\.png/"**
🔹Default: **null** | | **allow_reload** | Allow reloading attachments (🔶Frappe >= v13.0.0).

🔶 Affect the visibility of the reload button.🔶

🔹Default: **true** | | **allow_remove** | Allow removing and clearing attachments.

🔶 Affect the visibility of the remove and clear buttons.🔶

🔹Default: **true** | +| **users** 🔴 | Array of custom options for a specific user or group of users.

🔹Example: **[{"for": "Guest", "disabled": true}, {"for": ["Administrator", "user"], "allow_multiple": true}]**
🔹Default: **null** | +| **roles** 🔴 | Array of custom options for a specific role or group of roles.
⚠️ *(Custom options for users is prioritized over roles.)*

🔹Example: **[{"for": ["Administrator", "System"], "allow_multiple": true}]**
🔹Default: **null** | + +🔴 New - 🔶 Changed --- ### Available JavaScript Methods | Method | Description | | :--- | :--- | -| **auto_save(enable: Boolean)** | Enable/Disable form auto save after upload. | +| **toggle_auto_save(enable: Boolean !Optional)** 🔶 | Enable/Disable form auto save after upload. | | **toggle_reload(allow: Boolean !Optional)** | Allow/Deny reloading attachments and toggle the reload button (🔶Frappe >= v13.0.0). | | **toggle_remove(allow: Boolean !Optional)** | Allow/Deny removing and clearing attachments and toggle the clear and remove buttons. | | **set_options(options: JSON Object)** | Set or change the plugin options. | +🔴 New - 🔶 Changed + --- ### Supported Fields diff --git a/frappe_better_attach_control/public/js/controls/attach.js b/frappe_better_attach_control/public/js/controls/attach.js index 9b3cb6d..91dca89 100644 --- a/frappe_better_attach_control/public/js/controls/attach.js +++ b/frappe_better_attach_control/public/js/controls/attach.js @@ -166,13 +166,14 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro super.refresh(); if (Helpers.isString(this.df.options)) this.df.options = Helpers.parseJson(this.df.options, {}); - if (!Helpers.isPlainObject(this.df.options)) this.df.options = {}; + else if (!Helpers.isPlainObject(this.df.options)) this.df.options = {}; if (!Helpers.isEqual(this.df.options, this._ls_options)) - this.set_options(this.df.options); + this._update_options(true); } // Custom Methods - auto_save(enable) { - this._disable_auto_save = enable ? false : true; + toggle_auto_save(enable) { + if (enable != null) this._disable_auto_save = enable ? false : true; + else this._disable_auto_save = !this._disable_auto_save; } toggle_reload(allow) { if (allow != null) this._allow_reload = !!allow; @@ -187,8 +188,10 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro set_options(opts) { if (Helpers.isString(opts) && opts.length) opts = Helpers.parseJson(opts, null); if (Helpers.isEmpty(opts) || !Helpers.isPlainObject(opts)) return; - $.extend(true, this.df.options, opts); - this._update_options(); + opts = Helpers.merge(this.df.options, opts); + if (Helpers.isEqual(this.df.options, opts)) return; + this.df.options = opts; + this._update_options(true); } // Private Methods _setup_control() { @@ -231,16 +234,37 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro this.df.options = Helpers.parseJson(this.df.options, {}); if (!Helpers.isPlainObject(this.df.options)) this.df.options = {}; } - _update_options() { - this._ls_options = Helpers.deepClone(this.df.options); - let opts; - if (Helpers.isEmpty(this._ls_options)) opts = {}; - else opts = this._parse_options(this._ls_options); - this._options = opts.options || null; + _update_options(force) { + if (!force && this._ls_options) return; + this._ls_options = !Helpers.isEmpty(this.df.options) ? Helpers.deepClone(this.df.options) : {}; + let opts = {}; + if (!Helpers.isEmpty(this._ls_options)) { + opts = this._parse_options(this._ls_options); + if (!opts.disabled) { + if (Helpers.isArray(this._ls_options.users) && this._ls_options.users.length) { + let users = Helpers.filter(this._ls_options.users, function(v) { + return this.isPlainObject(v) && ( + (this.isString(v.for) && v.for === frappe.session.user) + || (this.isArray(v.for) && v.for.indexOf(frappe.session.user) >= 0) + ); + }); + if (users.length) opts = Helpers.merge(opts, this._parse_options(users[0])); + } else if (Helpers.isArray(this._ls_options.roles)) { + let roles = Helpers.filter(this._ls_options.roles, function(v) { + return this.isPlainObject(v) + && (this.isString(v.for) || this.isArray(v.for)) + && frappe.user.has_role(v.for); + }); + if (roles.length) opts = Helpers.merge(opts, this._parse_options(roles[0])); + } + } + } + this._options = !opts.disabled ? (opts.options || null) : null; this._reload_control(opts); } _parse_options(opts) { var tmp = {options: {restrictions: {}, extra: {}}}; + tmp.disabled = Helpers.toBool(Helpers.ifNull(opts.disabled, false)); tmp.allow_reload = Helpers.toBool(Helpers.ifNull(opts.allow_reload, true)); tmp.allow_remove = Helpers.toBool(Helpers.ifNull(opts.allow_remove, true)); Helpers.each([ @@ -279,22 +303,24 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro } _parse_allowed_file_types(opts) { opts.extra.allowed_file_types = []; - if (Helpers.isEmpty(opts.restrictions.allowed_file_types)) return; + if (!opts.restrictions.allowed_file_types.length) return; opts.restrictions.allowed_file_types = Helpers.filter( opts.restrictions.allowed_file_types, - function(v) { return this.isRegExp(v) || (this.isString(v) && v.length); } - ); - Helpers.each(opts.restrictions.allowed_file_types, function(t, i) { - if (this.isString(t)) { - if (t[0] === '$') t = new RegExp(t.substring(1)); - else if (t.substring(t.length - 2) === '/*') - t = new RegExp(t.substring(0, t.length - 1) + '/(.*?)'); + function(v) { + if (this.isString(v)) { + if (!v.length) return false; + if (v[0] === '$') { + opts.extra.allowed_file_types.push(new RegExp(v.substring(1))); + return false; + } + if (v.substring(v.length - 2) === '/*') + opts.extra.allowed_file_types.push(new RegExp(v.substring(0, v.length - 1) + '/(.*?)')); + return true; + } else if (this.isRegExp(v)) { + opts.extra.allowed_file_types.push(v); + } + return false; } - opts.extra.allowed_file_types.push(t); - }); - opts.restrictions.allowed_file_types = Helpers.filter( - opts.restrictions.allowed_file_types, - function(v) { return this.isString(v) && v[0] !== '$'; } ); } _toggle_remove_button() { diff --git a/frappe_better_attach_control/public/js/controls/v12/attach.js b/frappe_better_attach_control/public/js/controls/v12/attach.js index 6c524a9..6b87c24 100644 --- a/frappe_better_attach_control/public/js/controls/v12/attach.js +++ b/frappe_better_attach_control/public/js/controls/v12/attach.js @@ -147,14 +147,15 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlAttach.extend({ this._super(); if (Helpers.isString(this.df.options)) this.df.options = Helpers.parseJson(this.df.options, {}); - if (!Helpers.isPlainObject(this.df.options)) this.df.options = {}; + else if (!Helpers.isPlainObject(this.df.options)) this.df.options = {}; if (!Helpers.isEqual(this.df.options, this._ls_options)) - this.set_options(this.df.options); + this._update_options(true); this.set_input(Helpers.toArray(this.value)); }, // Custom Methods - auto_save: function(enable) { - this._disable_auto_save = enable ? false : true; + toggle_auto_save: function(enable) { + if (enable != null) this._disable_auto_save = enable ? false : true; + else this._disable_auto_save = !this._disable_auto_save; }, toggle_remove: function(allow) { if (allow != null) this._allow_remove = !!allow; @@ -164,8 +165,10 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlAttach.extend({ set_options: function(opts) { if (Helpers.isString(opts) && opts.length) opts = Helpers.parseJson(opts, null); if (Helpers.isEmpty(opts) || !Helpers.isPlainObject(opts)) return; - $.extend(true, this.df.options, opts); - this._update_options(); + opts = Helpers.merge(this.df.options, opts); + if (Helpers.isEqual(this.df.options, opts)) return; + this.df.options = opts; + this._update_options(true); }, // Private Methods _setup_control: function() { @@ -207,16 +210,37 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlAttach.extend({ this.df.options = Helpers.parseJson(this.df.options, {}); if (!Helpers.isPlainObject(this.df.options)) this.df.options = {}; }, - _update_options: function() { - this._ls_options = Helpers.deepClone(this.df.options); - let opts; - if (Helpers.isEmpty(this._ls_options)) opts = {}; - else opts = this._parse_options(this._ls_options); - this._options = opts.options || null; + _update_options: function(force) { + if (!force && this._ls_options) return; + this._ls_options = !Helpers.isEmpty(this.df.options) ? Helpers.deepClone(this.df.options) : {}; + let opts = {}; + if (!Helpers.isEmpty(this._ls_options)) { + opts = this._parse_options(this._ls_options); + if (!opts.disabled) { + if (Helpers.isArray(this._ls_options.users) && this._ls_options.users.length) { + let users = Helpers.filter(this._ls_options.users, function(v) { + return this.isPlainObject(v) && ( + (this.isString(v.for) && v.for === frappe.session.user) + || (this.isArray(v.for) && v.for.indexOf(frappe.session.user) >= 0) + ); + }); + if (users.length) opts = Helpers.merge(opts, this._parse_options(users[0])); + } else if (Helpers.isArray(this._ls_options.roles)) { + let roles = Helpers.filter(this._ls_options.roles, function(v) { + return this.isPlainObject(v) + && (this.isString(v.for) || this.isArray(v.for)) + && frappe.user.has_role(v.for); + }); + if (roles.length) opts = Helpers.merge(opts, this._parse_options(roles[0])); + } + } + } + this._options = !opts.disabled ? (opts.options || null) : null; this._reload_control(opts); }, _parse_options: function(opts) { var tmp = {options: {restrictions: {}, extra: {}}}; + tmp.disabled = Helpers.toBool(Helpers.ifNull(opts.disabled, false)); tmp.allow_remove = Helpers.toBool(Helpers.ifNull(opts.allow_remove, true)); Helpers.each([ ['upload_notes', 's'], ['disable_auto_save', 'b'], @@ -251,22 +275,24 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlAttach.extend({ }, _parse_allowed_file_types: function(opts) { opts.extra.allowed_file_types = []; - if (Helpers.isEmpty(opts.restrictions.allowed_file_types)) return; + if (!opts.restrictions.allowed_file_types.length) return; opts.restrictions.allowed_file_types = Helpers.filter( opts.restrictions.allowed_file_types, - function(v) { return this.isRegExp(v) || (this.isString(v) && v.length); } - ); - Helpers.each(opts.restrictions.allowed_file_types, function(t, i) { - if (this.isString(t)) { - if (t[0] === '$') t = new RegExp(t.substring(1)); - else if (t.substring(t.length - 2) === '/*') - t = new RegExp(t.substring(0, t.length - 1) + '/(.*?)'); + function(v) { + if (this.isString(v)) { + if (!v.length) return false; + if (v[0] === '$') { + opts.extra.allowed_file_types.push(new RegExp(v.substring(1))); + return false; + } + if (v.substring(v.length - 2) === '/*') + opts.extra.allowed_file_types.push(new RegExp(v.substring(0, v.length - 1) + '/(.*?)')); + return true; + } else if (this.isRegExp(v)) { + opts.extra.allowed_file_types.push(v); + } + return false; } - opts.extra.allowed_file_types.push(t); - }); - opts.restrictions.allowed_file_types = Helpers.filter( - opts.restrictions.allowed_file_types, - function(v) { return this.isString(v) && v[0] !== '$'; } ); }, _toggle_remove_button: function() { diff --git a/frappe_better_attach_control/public/js/controls/v13/attach.js b/frappe_better_attach_control/public/js/controls/v13/attach.js index d758292..5dc6e22 100644 --- a/frappe_better_attach_control/public/js/controls/v13/attach.js +++ b/frappe_better_attach_control/public/js/controls/v13/attach.js @@ -157,14 +157,15 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlAttach.extend({ this._super(); if (Helpers.isString(this.df.options)) this.df.options = Helpers.parseJson(this.df.options, {}); - if (!Helpers.isPlainObject(this.df.options)) this.df.options = {}; + else if (!Helpers.isPlainObject(this.df.options)) this.df.options = {}; if (!Helpers.isEqual(this.df.options, this._ls_options)) - this.set_options(this.df.options); + this._update_options(true); this.set_input(Helpers.toArray(this.value)); }, // Custom Methods - auto_save: function(enable) { - this._disable_auto_save = enable ? false : true; + toggle_auto_save: function(enable) { + if (enable != null) this._disable_auto_save = enable ? false : true; + else this._disable_auto_save = !this._disable_auto_save; }, toggle_reload: function(allow) { if (allow != null) this._allow_reload = !!allow; @@ -179,8 +180,10 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlAttach.extend({ set_options: function(opts) { if (Helpers.isString(opts) && opts.length) opts = Helpers.parseJson(opts, null); if (Helpers.isEmpty(opts) || !Helpers.isPlainObject(opts)) return; - $.extend(true, this.df.options, opts); - this._update_options(); + opts = Helpers.merge(this.df.options, opts); + if (Helpers.isEqual(this.df.options, opts)) return; + this.df.options = opts; + this._update_options(true); }, // Private Methods _setup_control: function() { @@ -223,16 +226,37 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlAttach.extend({ this.df.options = Helpers.parseJson(this.df.options, {}); if (!Helpers.isPlainObject(this.df.options)) this.df.options = {}; }, - _update_options: function() { - this._ls_options = Helpers.deepClone(this.df.options); - let opts; - if (Helpers.isEmpty(this._ls_options)) opts = {}; - else opts = this._parse_options(this._ls_options); - this._options = opts.options || null; + _update_options: function(force) { + if (!force && this._ls_options) return; + this._ls_options = !Helpers.isEmpty(this.df.options) ? Helpers.deepClone(this.df.options) : {}; + let opts = {}; + if (!Helpers.isEmpty(this._ls_options)) { + opts = this._parse_options(this._ls_options); + if (!opts.disabled) { + if (Helpers.isArray(this._ls_options.users) && this._ls_options.users.length) { + let users = Helpers.filter(this._ls_options.users, function(v) { + return this.isPlainObject(v) && ( + (this.isString(v.for) && v.for === frappe.session.user) + || (this.isArray(v.for) && v.for.indexOf(frappe.session.user) >= 0) + ); + }); + if (users.length) opts = Helpers.merge(opts, this._parse_options(users[0])); + } else if (Helpers.isArray(this._ls_options.roles)) { + let roles = Helpers.filter(this._ls_options.roles, function(v) { + return this.isPlainObject(v) + && (this.isString(v.for) || this.isArray(v.for)) + && frappe.user.has_role(v.for); + }); + if (roles.length) opts = Helpers.merge(opts, this._parse_options(roles[0])); + } + } + } + this._options = !opts.disabled ? (opts.options || null) : null; this._reload_control(opts); }, _parse_options: function(opts) { var tmp = {options: {restrictions: {}, extra: {}}}; + tmp.disabled = Helpers.toBool(Helpers.ifNull(opts.disabled, false)); tmp.allow_reload = Helpers.toBool(Helpers.ifNull(opts.allow_reload, true)); tmp.allow_remove = Helpers.toBool(Helpers.ifNull(opts.allow_remove, true)); Helpers.each([ @@ -268,22 +292,24 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlAttach.extend({ }, _parse_allowed_file_types: function(opts) { opts.extra.allowed_file_types = []; - if (Helpers.isEmpty(opts.restrictions.allowed_file_types)) return; + if (!opts.restrictions.allowed_file_types.length) return; opts.restrictions.allowed_file_types = Helpers.filter( opts.restrictions.allowed_file_types, - function(v) { return this.isRegExp(v) || (this.isString(v) && v.length); } - ); - Helpers.each(opts.restrictions.allowed_file_types, function(t, i) { - if (this.isString(t)) { - if (t[0] === '$') t = new RegExp(t.substring(1)); - else if (t.substring(t.length - 2) === '/*') - t = new RegExp(t.substring(0, t.length - 1) + '/(.*?)'); + function(v) { + if (this.isString(v)) { + if (!v.length) return false; + if (v[0] === '$') { + opts.extra.allowed_file_types.push(new RegExp(v.substring(1))); + return false; + } + if (v.substring(v.length - 2) === '/*') + opts.extra.allowed_file_types.push(new RegExp(v.substring(0, v.length - 1) + '/(.*?)')); + return true; + } else if (this.isRegExp(v)) { + opts.extra.allowed_file_types.push(v); + } + return false; } - opts.extra.allowed_file_types.push(t); - }); - opts.restrictions.allowed_file_types = Helpers.filter( - opts.restrictions.allowed_file_types, - function(v) { return this.isString(v) && v[0] !== '$'; } ); }, _toggle_remove_button: function() { diff --git a/frappe_better_attach_control/public/js/utils/index.js b/frappe_better_attach_control/public/js/utils/index.js index c6af81d..1c5ce92 100644 --- a/frappe_better_attach_control/public/js/utils/index.js +++ b/frappe_better_attach_control/public/js/utils/index.js @@ -15,6 +15,7 @@ var Helpers = { $of: function(v, t) { return this.$type(v) === t; }, $ofAny: function(v, t) { return t.split(' ').indexOf(this.$type(v)) >= 0; }, $propOf: function(v, k) { return Object.prototype.hasOwnProperty.call(v, k); }, + $fnStr: function(v) { return Function.prototype.toString.call(v); }, // Common Checks isString: function(v) { return this.$of(v, 'String'); }, @@ -24,20 +25,26 @@ var Helpers = { return this.isNumber(v) && v >= 0 && v % 1 == 0 && v <= 9007199254740991; }, isInteger: function(v) { return this.isNumber(v) && v === Number(parseInt(v)); }, + isFunction: function(v) { return typeof v === 'function' || /(Function|^Proxy)$/.test(this.$type(v)); }, isArrayLike: function(v) { - return v && !this.isString(v) && !$.isFunction(v) && !$.isWindow(v) - && this.isObjectLike(v) && !this.isInteger(v.nodeType) && this.isLength(v.length); + return this.isObjectLike(v) && !this.isFunction(v) && !this.$ofAny(v, 'String Window') + && v !== window && !/^(NodeList|HTML(\w+|)Collection)$/.test(this.$type(v)) + && !this.isInteger(v.nodeType) && this.isLength(v.length); }, - isFunction: function(v) { return v && $.isFunction(v); }, // Checks - isArray: function(v) { return v && $.isArray(v); }, + isArray: function(v) { return this.$of(v, 'Array'); }, isObject: function(v) { return this.isObjectLike(v) - && this.isObjectLike(Object.getPrototypeOf(Object(v)) || {}) - && !this.$ofAny(v, 'String Number Boolean Array RegExp Date URL'); + && !this.$ofAny(v, 'String Number Boolean Array RegExp Date URL') + && this.isObjectLike(Object.getPrototypeOf(v)); + }, + isPlainObject: function(v) { + if (!this.isObject(v)) return false; + let k = 'constructor'; v = Object.getPrototypeOf(v); + return v && this.$propOf(v, k) && this.isFunction(v[k]) + && this.$fnStr(v[k]) === this.$fnStr(Object); }, - isPlainObject: function(v) { return v && $.isPlainObject(v); }, isIteratable: function(v) { return this.isArrayLike(v) || this.isObject(v); }, isEmpty: function(v) { if (v == null) return true; @@ -103,7 +110,12 @@ var Helpers = { }, deepClone: function(v) { if (!this.isIteratable(v)) return v; - var arr = this.isArrayLike(v), + var ret = this.toJson(v); + if (ret.length) { + ret = this.parseJson(ret, null); + if (this.isIteratable(ret)) return ret; + } + var arr = this.isArrayLike(v); ret = arr ? [] : {}; this.each(v, function(y, x) { if (this.isIteratable(y)) y = this.deepClone(y); @@ -111,8 +123,22 @@ var Helpers = { }); return ret; }, + merge: function(b) { + if (!this.isIteratable(b)) return arguments[1] || null; + b = this.deepClone(b); + this.each(arguments, function(a, i) { + i && this.each(a, function(y, x) { + if (this.isObject(y) && this.isObject(b[x])) y = this.merge(b[x], y); + if (!this.isArrayLike(b[x])) b[x] = y; + else if (!this.isArrayLike(y)) Array.prototype.push.call(b[x], y); + else Array.prototype.push.apply(b[x], y); + }); + }); + return b; + }, isEqual: function(data, base) { - if (!this.isIteratable(data) || !this.isIteratable(base)) return data == base; + if (this.isIteratable(data) !== this.isIteratable(base)) return data == base; + if (this.isEmpty(data) && this.isEmpty(base)) return this.$type(data) === this.$type(base); var ret = true; this.each(data, function(v, k) { if (!this.isEqual(v, base[k])) return (ret = false);