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);