Comparing version 0.4.0 to 0.5.0
{ | ||
"name": "corridor", | ||
"version": "0.4.0", | ||
"description": "JSON/HTML data corridor for data-binding", | ||
"version": "0.5.0", | ||
"description": "JSON -> HTML -> JSON data corridor", | ||
"repository": { | ||
@@ -9,5 +9,7 @@ "type": "git", | ||
}, | ||
"main": "./src/corridor.js", | ||
"keywords": [ | ||
"json", | ||
"dom", | ||
"template", | ||
"data-binding", | ||
@@ -14,0 +16,0 @@ "corridor" |
227
README.md
@@ -10,5 +10,5 @@ # corridor | ||
Your data is in JSON, but your users interact with HTML. | ||
corridor's only mission is to shuttle your data between your JSON and your HTML. | ||
corridor shuttles your data between your JSON and your HTML. | ||
In a nutshell, corridor gives you the power to turn this: | ||
In a nutshell, corridor lets you turn this: | ||
@@ -51,5 +51,5 @@ ```html | ||
* **unobtrusive** — corridor presents just one function, has no dependencies, and causes no side-effects. | ||
* **intelligent** — corridor learns what to do by looking at the data, not by being told (except where you want to). | ||
* **clear** — corridor's code, functionality, tests, milestones and issues are all well documented and easy to follow. | ||
* **intelligent** — it learns what to do by looking at the data on the ground. | ||
* **unobtrusive** — it's just one function, without dependencies or side-effects. | ||
* **clear** — corridor's code, tests, and issues are crisply documented. | ||
@@ -410,3 +410,3 @@ Development of features, bugfixes and documentation are held to these ideals. | ||
Adding the `toggleable` role to the `<frameset>` signals to corridor that this section can be turned on and off. | ||
Adding the `toggleable` role to the `<fieldset>` signals to corridor that this section can be turned on and off. | ||
The checkbox with the role `toggle` controls it. | ||
@@ -419,3 +419,4 @@ | ||
This tutorial has focused largely on explaining how data flows from HTML to JSON, but corridor is great at sending data the other way as well. | ||
So far, this tutorial has focused on explaining how data flows from HTML to JSON. | ||
But corridor is great at sending data the other way as well. | ||
@@ -433,2 +434,67 @@ To insert data back into the DOM, call the corridor function with a root element and a data structure object. | ||
### expanding to fit | ||
When you send data from JSON into HTML, there's a chance that there won't be enough room. | ||
This is especilly true when working with arrays. | ||
Consider this input JSON: | ||
```js | ||
{ | ||
"company": { | ||
"employees": [{ | ||
"name": "Bob", | ||
"email": "bob@company.com" | ||
},{ | ||
"name": "Alice", | ||
"email": "alice@company.com" | ||
}] | ||
} | ||
} | ||
``` | ||
And this HTML: | ||
```html | ||
<table> | ||
<tr data-name="company.employees[]"> | ||
<td><input type="text" name="name" /></td> | ||
<td><input type="text" name="email" /></td> | ||
</tr> | ||
</table> | ||
``` | ||
If you ran corridor in insert mode in this scenario, Alice would be lost since there's only one row for `company.employees`. | ||
Fortunately, corridor can expand the DOM for you to make room for the extra elements. | ||
If you set `data-expand` to `auto` on a named element, corridor will duplicate it to make room for data that otherwise wouldn't fit. | ||
Here's the same example again, with the `data-expand` attribute set: | ||
```html | ||
<table> | ||
<tr data-name="company.employees[]" data-expand="auto"> | ||
<td><input type="text" name="name" /></td> | ||
<td><input type="text" name="email" /></td> | ||
</tr> | ||
</table> | ||
``` | ||
After calling `corridor(table, data)`, the HTML in the page will look like this: | ||
```html | ||
<table> | ||
<tr data-name="company.employees[]" data-expand="auto"> | ||
<td><input type="text" name="name" value="Bob" /></td> | ||
<td><input type="text" name="email" value="bob@company.com" /></td> | ||
</tr> | ||
<tr data-name="company.employees[]" data-expand="auto"> | ||
<td><input type="text" name="name" value="Alice" /></td> | ||
<td><input type="text" name="email" value="alice@company.com" /></td> | ||
</tr> | ||
</table> | ||
``` | ||
Since the expand feature is more intrusive in its side-effects in the DOM, you must enable it explicitly either by setting the `data-enabled` option, or in the options argument to the `corridor()` function. | ||
## corridor API | ||
@@ -518,5 +584,14 @@ | ||
Options that apply to any field are `type` and `empty`. | ||
The `role` and `enabledOnly` options apply only to toggleable/toggle functionality. | ||
Available options are: | ||
* `type` - the type of the field (_auto_, _string_, _boolean_, _number_, _list_, or _json_) | ||
* `empty` - whether to include the value in the output if the field is empty (_auto_, _include_, or _omit_) | ||
* `merge` - strategy to use when merging arrays (_auto_, _concat_, or _extend_) | ||
* `include` - whether a non-form element should be considered for insert/extract (_auto_, _always_, or _never_) | ||
* `extract` - strategy for pulling a value from a non-form element when extracting (_auto_, _value_, _text_, or _html_) | ||
* `insert` - strategy for putting a value into a non-form element when inserting (_auto_, _value_, _text_, or _html_) | ||
* `expand` - whether to expand the DOM to accomodate data that otherwise wouldn't fit (_never_, _auto_) | ||
* `role` - what role this element plays (_field_, _toggleable_, _toggle_, _expand_) | ||
* `enabledOnly` - only include enabled elements for consideration during insert/extract (_true_, _false_) | ||
Note that setting options via the `opts` param specifically affects the execution of the corridor function just once. | ||
@@ -532,8 +607,8 @@ Persistent options should be stored in the HTML. | ||
* _auto_ - automatically detect the correct type based on the value (default) | ||
* _string_ - treat the value as a string | ||
* _boolean_ - coerce this value to something true/false | ||
* _number_ - parse this value as a number | ||
* _json_ - leave this value as-is (will choke if it's not actually valid JSON) | ||
* _list_ - parse this value as a list of values | ||
* `auto` - automatically detect the correct type based on the value (default) | ||
* `string` - treat the value as a string | ||
* `boolean` - coerce this value to something true/false | ||
* `number` - parse this value as a number | ||
* `json` - leave this value as-is (will choke if it's not actually valid JSON) | ||
* `list` - parse this value as a list of values | ||
@@ -554,5 +629,5 @@ When automatically detecting the type, corridor uses the following algorithm: | ||
* _auto_ - automatically detect the appropriate behavior based on the circumstances (default) | ||
* _include_ - include the value in the output (default) | ||
* _omit_ - do not add the field at all | ||
* `auto` - automatically detect the appropriate behavior based on the circumstances (default) | ||
* `include` - include the value in the output (default) | ||
* `omit` - do not add the field at all | ||
@@ -568,3 +643,3 @@ When empty is set to `auto`, corridor uses the following algorithm to between `include` and `omit`: | ||
##### merge options | ||
##### merge option | ||
@@ -575,5 +650,5 @@ The `merge` option indicates which merging strategy corridor should use when merging two arrays. | ||
* _auto_ - intelligently choose whether to concatenate the arrays, or deep merge them (default) | ||
* _concat_ - concatenate the arrays | ||
* _extend_ - deep merge each pair of items | ||
* `auto` - intelligently choose whether to concatenate the arrays, or deep merge them (default) | ||
* `concat` - concatenate the arrays | ||
* `extend` - deep merge each pair of items | ||
@@ -603,2 +678,105 @@ When in `auto` mode, the algorithm for choosing whether to concatenate or merge two arrays should work as follows: | ||
##### include option | ||
The `include` option indicates whether a non-form element should be considered for insert/extract. | ||
Choices are: | ||
* `auto` - intelligently choose whether to include the element (default). | ||
* `always` - always include the element. | ||
* `never` - never include the element. | ||
When operating in _auto_ mode, corridor uses the following algorithm to decide whether a non-form element should be considered for insert/extract: | ||
* if the element is a form field (`input`, `textarea`, `select`), include it, otherwise, | ||
* if the element has no children with `name` or `data-name` attributes, include it, otherwise, | ||
* exclude it. | ||
Only elements included by this algorithm will contribute to extracted output or receive inserted data. | ||
##### extract option | ||
The `extract` option indicates how a value should be extracted from an element under consideration. | ||
Choices are: | ||
* `auto` - intelligently choose the best way to get a value (default). | ||
* `value` - get the element's form value (`value` attribute, except for `<select>` elements). | ||
* `text` - get the element's `textContent`. | ||
* `html` - get the element's `innerHTML`. | ||
When operating in _auto_ mode, corridor uses the following algorithm to decide how to extract a value: | ||
* if the element is a form field, use _value_ extraction, otherwise, | ||
* if the element has no child elements (only text), use _text_, otherwise, | ||
* if the element is a `<pre>` or `<code>` element, use _text_, otherwise, | ||
* use _html_. | ||
If you set the extract option, it's much better to set it in the HTML specifically for a particular element. | ||
Most of the time, you'll want the _auto_ detection. | ||
##### insert option | ||
The `insert` option indicates how a value should be inserted into an element under consideration. | ||
Choices are: | ||
* `auto` - intelligently choose the best way to insert the value (default). | ||
* `value` - set the element's form value (`value` attribute, except for `<select>` elements). | ||
* `text` - set the element's `textContent`. | ||
* `html` - set the element's `innerHTML`. | ||
When operating in _auto_ mode, corridor uses the following algorithm to decide how to insert a value: | ||
* if the element is a form field, use _value_ insertion, otherwise, | ||
* if the value appears to contain no HTML elements, use _text_, otherwise, | ||
* if the element is a `<pre>` or `<code>` element, use _text_, otherwise, | ||
* use _html_. | ||
If you set the insert option, it's much better to set it in the HTML specifically for a particular element. | ||
Most of the time, you'll want the _auto_ detection. | ||
##### expand option | ||
The `expand` option indicates whether corridor should make any attempt to expand the DOM to accomodate data that otherwise wouldn't fit. | ||
Choices are: | ||
* `never` - do not add any elements to the DOM (default). | ||
* `auto` - intelligently choose the best way to expand the DOM if necessary. | ||
When operating in _auto_ mode, corridor uses the following algorithm to decide how to expand the DOM: | ||
* identify candidates for expansion; these are elements which: | ||
- have a `name` or `data-name` attribute, | ||
- don't a `data-expand` value of `never`, | ||
- have a name format that ends in `[]`, | ||
- are not the children of such an element (the algorithm does _not_ recurse) | ||
* for each set of candidates: | ||
- find the matching array from the source data, | ||
- if there source data array and list of candidate fields have the same length, abort, otherwise, | ||
- find the best target to clone, then, | ||
- create N clone siblings of the target, appending each sequentially to the target's parent element, where N is the difference between the data array length and the length of the list of candidate elements. | ||
The algorithm for finding a clone target for a set of candidate elements is: | ||
* start with the last element in the list of candidates (called `elem` here), | ||
* if that element is not a value'd form field: | ||
- check for a child with `data-role` set to `expand`, if found, use it | ||
* if the element `elem` is a value'd form field: | ||
- walk up the DOM looking for a parent with `data-role` set to `expand`, if found, use it, otherwise, | ||
- walk up the DOM looking for either a `<li>` element or a `<tr>`, if found, use it | ||
* last resort, use the element `elem` | ||
Known limitations: | ||
* Only the last candidate element is searched for a target to clone. It's not round-robin or based on the content of the data array. | ||
* The shortfall is calculated from the element which starts the process, not the target element. | ||
* The algorithm doesn't recurse, so nested expand elements will not be expanded. | ||
Improving on these limitations will require a major update to the code base to include an intermediate tree structure. | ||
This tree structure would provide the rich information necessary to make more intelligent auto-expand decisions. | ||
See [Issue #39](https://github.com/jimbojw/corridor/issues/39). | ||
##### toggle options | ||
@@ -744,2 +922,4 @@ | ||
_Note: field format will probably be removed in a future version of corridor_ | ||
Whereas name format resembles how you'd _access_ an object in JavaScript, field format resembles how you describe an object in JavaScript—that is, JSON. | ||
@@ -763,4 +943,3 @@ | ||
When possible, you should use the name format for your name attributes. | ||
But there are some rare cases where field format is the better option. | ||
You should use the name format for your name attributes. | ||
@@ -767,0 +946,0 @@ #### data-opts attribute |
@@ -133,3 +133,6 @@ /** | ||
data = {}, | ||
fields = selectFields(root, settings).filter(hasVal); | ||
fields = selectFields(root, settings) | ||
.filter(function(elem) { | ||
return hasVal(elem, settings); | ||
}); | ||
@@ -139,3 +142,3 @@ if (settings.enabledOnly) { | ||
} | ||
fields.forEach(function(elem) { | ||
@@ -146,9 +149,7 @@ | ||
value = val(elem), | ||
contrib, | ||
field; | ||
// build out full contribution | ||
contrib = buildup("\ufffc", elem, root), | ||
field = contrib.split("\ufffc").join('$$$'); | ||
// build out full contribution | ||
contrib = buildup("\ufffc", elem, root); | ||
field = contrib.split("\ufffc").join('$$$'); | ||
// short-circuit if this field should be omitted | ||
@@ -190,2 +191,5 @@ if (!value && !includeEmpty(field, elem, opts)) { | ||
// expand DOM to fit data | ||
expand(root, data, opts); | ||
var | ||
@@ -199,3 +203,6 @@ | ||
fields = selectFields(root, settings).filter(hasVal); | ||
fields = selectFields(root, settings) | ||
.filter(function(elem) { | ||
return hasVal(elem, settings); | ||
}); | ||
@@ -262,2 +269,55 @@ if (settings.enabledOnly) { | ||
/** | ||
* Expand the DOM to fit the supplied data. | ||
* @param {HTMLElement} root The element to scan for insertion fields (optional). | ||
* @param {mixed} data The data to insert. | ||
* @param {object} opts Hash of options (optional, see corridor options) | ||
*/ | ||
expand = corridor.expand = function(root, data, opts) { | ||
var | ||
settings = options(root, extend({}, defaults, opts)), | ||
// search for candidates to expand | ||
candidates = findExpandCandidates(root, settings); | ||
keys(candidates).forEach(function(field){ | ||
var candidate, arry, shortfall, target, parent, sibling, cloneElem; | ||
// grab candidate | ||
candidate = candidates[field]; | ||
// find array in the data that maps to this candidate | ||
arry = follow(candidate.path, data); | ||
if (!arry || !arry.length) { | ||
return; | ||
} | ||
// compare length of elems to data mapped array to determine shortfall | ||
shortfall = arry.length - candidate.elems.length; | ||
if (shortfall === 0) { | ||
return; | ||
} | ||
// choose best element for clone target | ||
target = findExpandTarget(candidate.elems[candidate.elems.length - 1], root, settings); | ||
if (!target) { | ||
return; | ||
} | ||
sibling = target.nextSibling; | ||
parent = target.parentNode; | ||
// clone last element N times | ||
while (shortfall--) { | ||
cloneElem = target.cloneNode(true); | ||
parent.insertBefore(cloneElem, sibling); | ||
sibling = cloneElem.nextSibling; | ||
} | ||
}); | ||
}, | ||
/** | ||
* Default values applied to options. | ||
@@ -282,2 +342,3 @@ */ | ||
* - toggle - this element is a checkbox which toggles its nearest parent toggleable | ||
* - expand - this element is meant to be expanded (cloned) to accomodate data in the case of a shortfall | ||
*/ | ||
@@ -310,7 +371,152 @@ role: "field", | ||
*/ | ||
merge: 'auto' | ||
merge: 'auto', | ||
/** | ||
* Whether to include a non-form element when inserting/extracting. | ||
* - auto - intelligently decide whether each element should be included (default) | ||
* - always - always include the element for value consideration | ||
* - never - never include this element for value consideration | ||
*/ | ||
include: 'auto', | ||
/** | ||
* Strategy for pulling a value out of a non-form element when extracting. | ||
* - auto - intelligently decide how each element's value should be extracted (default) | ||
* - value - use the value attribute (or equivalent for <select> elements) | ||
* - text - use the element's textContent | ||
* - html - use the element's innerHTML | ||
*/ | ||
extract: 'auto', | ||
/** | ||
* Strategy for setting a value into a non-form element when inserting. | ||
* - auto - intelligently decide how each element should receive the value (default) | ||
* - value - set the value attribute (or equivalent for <select> elements) | ||
* - text - set the element's textContent | ||
* - html - set the element's innerHTML | ||
*/ | ||
insert: 'auto', | ||
/** | ||
* Strategy for expanding the DOM to accomodate arrays of data. | ||
* - never - do not modify the DOM to try and accomodate data (default) | ||
* - auto - intelligently decide whether to expand based on circumstances | ||
*/ | ||
expand: 'never' | ||
}, | ||
/** | ||
* Given an element, find candidates for DOM expanssion. | ||
* Once a candidate is found, it is not recursively searched. | ||
* @param {HTMLElement} root The element to start from. | ||
* @param {object} opts Options (optional). | ||
*/ | ||
findExpandCandidates = corridor.expand.findExpandCandidates = function(root, opts) { | ||
var | ||
settings = extend({}, defaults, opts), | ||
queue = [root], | ||
candidates = {}, | ||
ufc = JSON.stringify("\ufffc"), | ||
// iteration vars | ||
elem, field, path, candidate; | ||
while (queue.length) { | ||
elem = queue.shift(); | ||
if (elem.hasAttribute('name') || elem.hasAttribute('data-name')) { | ||
if (options(elem, settings).expand !== 'never') { | ||
field = buildup(ufc, elem, root); | ||
if (field.indexOf("["+ufc+"]") !== -1) { | ||
path = locate(JSON.parse(field), "\ufffc").slice(0, -1); | ||
candidate = candidates[field]; | ||
if (!candidate) { | ||
candidate = candidates[field] = { | ||
path: path, | ||
elems: [] | ||
}; | ||
} | ||
candidate.elems.push(elem); | ||
} | ||
} | ||
} | ||
arrayify(elem.childNodes).forEach(function(child) { | ||
if (child.nodeType === 1) { | ||
queue.push(child); | ||
} | ||
}); | ||
} | ||
return candidates; | ||
}, | ||
/** | ||
* Given an element, intelligently find the best choice for DOM expanssion. | ||
* @param {HTMLElement} elem The element to start from. | ||
* @param {HTMLElement} root The highest element to consider. | ||
* @param {object} opts Options (optional). | ||
*/ | ||
findExpandTarget = corridor.expand.findExpandTarget = function(elem, root, opts) { | ||
var target; | ||
// non-value'd elements inspect downwards | ||
if (!hasVal(elem)) { | ||
// check for child with the data-role expand | ||
target = null; | ||
arrayify(elem.querySelectorAll('[data-role], [data-opts]')) | ||
.forEach(function(child) { | ||
if (!target) { | ||
if (options(child).role === 'expand') { | ||
target = child; | ||
} | ||
} | ||
}); | ||
return target || elem; | ||
} | ||
// walk up the DOM looking for an ancestor with the exand role | ||
target = null; | ||
upwalk(elem, root, function(parent, field, opts){ | ||
if (opts.role === 'expand') { | ||
target = parent; | ||
return false; | ||
} | ||
}); | ||
if (target) { | ||
return target; | ||
} | ||
// if there are no ancestors with role expand, look for special elements | ||
var node = elem; | ||
while (node) { | ||
if ((/^(tr|li)$/i).test(node.tagName)) { | ||
return node; | ||
} | ||
node = node === root ? null : node.parentNode; | ||
} | ||
// couldn't find a better target than the element itself | ||
return elem; | ||
}, | ||
/** | ||
* Grab the keys of an object as an array. | ||
*/ | ||
keys = corridor.keys = Object.keys || function(obj) { | ||
var ret = [], key; | ||
for (key in obj) { | ||
if (obj.hasOwnProperty(key)) { | ||
ret.push(key); | ||
} | ||
} | ||
return arrayify(ret); | ||
}, | ||
/** | ||
* Select an array of field elements from the specified root element. | ||
@@ -480,2 +686,3 @@ * @param {HTMLElement} root The root element to search for fields. | ||
follow = corridor.follow = function(path, node) { | ||
path = path.slice(0); | ||
while (node && path.length) { | ||
@@ -673,8 +880,9 @@ node = node[path.shift()]; | ||
* @param {mixed} value The value to set (optional). | ||
* @param {mixed} opts Options to pass to override defaults (optional). | ||
*/ | ||
val = corridor.val = function(elem, value) { | ||
val = corridor.val = function(elem, value, opts) { | ||
if (value === undefined) { | ||
return getVal(elem); | ||
return getVal(elem, opts); | ||
} | ||
return setVal(elem, value); | ||
return setVal(elem, value, opts); | ||
}, | ||
@@ -685,4 +893,31 @@ | ||
* @param {HTMLElement} elem The element whose value is to be determined. | ||
* @param {mixed} opts Options to pass to override defaults (optional). | ||
*/ | ||
getVal = val.getVal = function(elem) { | ||
getVal = val.getVal = function(elem, opts) { | ||
opts = options(elem, extend({}, defaults, opts)); | ||
return ( | ||
opts.extract === 'value' ? getFormVal(elem) : | ||
opts.extract === 'text' ? getText(elem) : | ||
opts.extract === 'html' ? elem.innerHTML : | ||
isValued(elem) ? getFormVal(elem) : | ||
elem.querySelectorAll('*').length === 0 ? getText(elem) : | ||
(/^(pre|code)$/i).test(elem.tagName) ? getText(elem) : | ||
elem.innerHTML | ||
); | ||
}, | ||
/** | ||
* Grab the text content of an element. | ||
* @param {HTMLElement} elem The element. | ||
* @return {string} The text content of the element. | ||
*/ | ||
getText = val.getText = function(elem) { | ||
return elem.textContent || elem.innerText || ''; | ||
}, | ||
/** | ||
* Get the value of the specified form element. | ||
* @param {HTMLElement} elem The element whose value is to be determined. | ||
*/ | ||
getFormVal = val.getFormVal = function(elem) { | ||
@@ -710,17 +945,62 @@ var | ||
* @param {HTMLElement} elem The element whose value is to be set. | ||
* @param {mixed} value The value to set (optional). | ||
* @param {mixed} value The value to set. | ||
* @param {mixed} opts Options to pass to override defaults (optional). | ||
*/ | ||
setVal = val.setVal = function(elem, value) { | ||
elem.value = value; | ||
setVal = val.setVal = function(elem, value, opts) { | ||
opts = options(elem, extend({}, defaults, opts)); | ||
if (opts.insert === 'value') { | ||
elem.value = value; | ||
} else if (opts.insert === 'text') { | ||
setText(elem, value); | ||
} else if (opts.insert === 'html') { | ||
elem.innerHTML = value; | ||
} else if (isValued(elem)) { | ||
elem.value = value; | ||
} else if (!(/<(\w+)\b[^>]*>/i).test(value)) { | ||
setText(elem, value); | ||
} else if ((/^(pre|code)$/i).test(elem.tagName)) { | ||
setText(elem, value); | ||
} else { | ||
elem.innerHTML = value; | ||
} | ||
}, | ||
/** | ||
* Determine whether a specified element could have or receive a val. | ||
* Set the text of an element to the given string. | ||
* @param {HTMLElement} elem The element whose test is to be set. | ||
* @param {string} text The text to set. | ||
*/ | ||
setText = val.setText = function(elem, text) { | ||
if ('textContent' in elem) { | ||
elem.textContent = text; | ||
} | ||
if ('innerText' in elem) { | ||
elem.innerText = text; | ||
} | ||
}, | ||
/** | ||
* Determine whether a specified element could receive or produce a value. | ||
* @param {HTMLElement} elem The element to test. | ||
* @param {mixed} opts Options to merge into element's options. | ||
*/ | ||
hasVal = val.hasVal = function(elem) { | ||
return (/^(input|textarea|select)$/i).test(elem.tagName.toLowerCase()); | ||
hasVal = val.hasVal = function(elem, opts) { | ||
opts = options(elem, extend({}, defaults, opts)); | ||
return ( | ||
opts.include === 'always' ? true : | ||
opts.include === 'never' ? false : | ||
isValued(elem) ? true : | ||
elem.querySelectorAll('*').length === 0 | ||
); | ||
}, | ||
/** | ||
* Determine whether a specified element is a value'd form element. | ||
* @param {HTMLElement} elem The element to test. | ||
*/ | ||
isValued = val.isValued = function(elem) { | ||
return (/^(input|textarea|select)$/i).test(elem.tagName); | ||
}, | ||
/** | ||
* Copy properties from one or more objects onto a target. | ||
@@ -901,2 +1181,11 @@ */ | ||
}, | ||
/** | ||
* Create a deep clone of a plain object. | ||
* @param {mixed} obj The object to clone. | ||
* @return {mixed} A deep clone of the original object. | ||
*/ | ||
clone = corridor.clone = function(obj) { | ||
return JSON.parse(JSON.stringify(obj)); | ||
}; | ||
@@ -903,0 +1192,0 @@ |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
166324
20
2010
994