From 00baaacaccd802eb19bfc540d87003e7e760e291 Mon Sep 17 00:00:00 2001 From: jos Date: Sat, 30 Mar 2019 13:35:51 +0100 Subject: [PATCH 1/7] Disable having duplicate nodes (except whilst typing) --- src/js/Node.js | 96 +++++++++++++++++++++------------------------- src/js/treemode.js | 5 +-- src/js/util.js | 20 ++++++++++ test/util.test.js | 46 ++++++++++++++++++++++ 4 files changed, 111 insertions(+), 56 deletions(-) diff --git a/src/js/Node.js b/src/js/Node.js index d3dfa41..e7c8a0a 100644 --- a/src/js/Node.js +++ b/src/js/Node.js @@ -379,7 +379,7 @@ Node.prototype.setField = function(field, fieldEditable) { */ Node.prototype.getField = function() { if (this.field === undefined) { - this._getDomField(); + this._getDomField(false); } return this.field; @@ -1891,13 +1891,25 @@ Node.prototype._getDomField = function(silent) { this.fieldInnerText = util.getInnerText(this.dom.field); } - if (this.fieldInnerText != undefined) { + if (this.fieldInnerText !== undefined) { try { var field = this._unescapeHTML(this.fieldInnerText); if (field !== this.field) { - this.field = field; - this._debouncedOnChangeField(); + var existingFieldNames = this.parent.getFieldNames(this); + var isDuplicate = existingFieldNames.indexOf(field) !== -1; + + if (!isDuplicate) { + this.field = field; + this._debouncedOnChangeField(); + } + else { + if (!silent) { + // fix duplicate field: change it into a unique name + this.field = util.findUniqueName(field, existingFieldNames); + this._debouncedOnChangeField(); + } + } } } catch (err) { @@ -1936,54 +1948,6 @@ Node.prototype._updateDomDefault = function () { } }; -/** - * Validate this node and all it's childs - * @return {Array.<{node: Node, error: {message: string}}>} Returns a list with duplicates - */ -Node.prototype.validate = function () { - var errors = []; - - // find duplicate keys - if (this.type === 'object') { - var keys = {}; - var duplicateKeys = []; - for (var i = 0; i < this.childs.length; i++) { - var child = this.childs[i]; - if (keys.hasOwnProperty(child.field)) { - duplicateKeys.push(child.field); - } - keys[child.field] = true; - } - - if (duplicateKeys.length > 0) { - errors = this.childs - .filter(function (node) { - return duplicateKeys.indexOf(node.field) !== -1; - }) - .map(function (node) { - return { - node: node, - error: { - message: translate('duplicateKey') + ' "' + node.field + '"' - } - } - }); - } - } - - // recurse over the childs - if (this.childs) { - for (var i = 0; i < this.childs.length; i++) { - var e = this.childs[i].validate(); - if (e.length > 0) { - errors = errors.concat(e); - } - } - } - - return errors; -}; - /** * Clear the dom of the node */ @@ -2958,6 +2922,13 @@ Node.prototype.onEvent = function (event) { if (target == domField) { switch (type) { case 'blur': + this._getDomField(false); + this._updateDomField(); + if (this.field) { + domField.innerHTML = this._escapeHTML(this.field); + } + break; + case 'change': this._getDomField(true); this._updateDomField(); @@ -3462,6 +3433,25 @@ Node.prototype._showColorPicker = function () { } }; +/** + * Get all field names of an object + * @param {Node} [excludeNode] Optional node to be excluded from the returned field names + * @return {string[]} + */ +Node.prototype.getFieldNames = function (excludeNode) { + if (this.type === 'object') { + return this.childs + .filter(function (child) { + return child !== excludeNode; + }) + .map(function (child) { + return child.field; + }); + } + + return []; +} + /** * Remove nodes * @param {Node[] | Node} nodes @@ -3526,6 +3516,8 @@ Node.onDuplicate = function(nodes) { var afterNode = lastNode; var clones = nodes.map(function (node) { var clone = node.clone(); + var existingFieldNames = node.parent.getFieldNames(); + clone.field = util.findUniqueName(node.field, existingFieldNames); parent.insertAfter(clone, afterNode); afterNode = clone; return clone; diff --git a/src/js/treemode.js b/src/js/treemode.js index 2122682..10f73b4 100644 --- a/src/js/treemode.js +++ b/src/js/treemode.js @@ -577,9 +577,6 @@ treemode.validate = function () { var json = root.getValue(); - // check for duplicate keys - var duplicateErrors = root.validate(); - // execute JSON schema validation var schemaErrors = []; if (this.validateSchema) { @@ -611,7 +608,7 @@ treemode.validate = function () { .then(function (customValidationErrors) { // only apply when there was no other validation started whilst resolving async results if (seq === me.validationSequence) { - var errorNodes = [].concat(duplicateErrors, schemaErrors, customValidationErrors || []); + var errorNodes = [].concat(schemaErrors, customValidationErrors || []); me._renderValidationErrors(errorNodes); } }) diff --git a/src/js/util.js b/src/js/util.js index bc14c3f..6bb4c94 100644 --- a/src/js/util.js +++ b/src/js/util.js @@ -1150,3 +1150,23 @@ exports.makeFieldTooltip = function (schema, locale) { return tooltip; } + +/** + * Find a unique name. Suffix the name with ' (copy)', '(copy 2)', etc + * until a unique name is found + * @param {string} name + * @param {Array} existingPropNames Array with existing prop names + */ +exports.findUniqueName = function(name, existingPropNames) { + var strippedName = name.replace(/ \(copy( \d+)?\)$/, '') + var validName = strippedName + var i = 1 + + while (existingPropNames.indexOf(validName) !== -1) { + var copy = 'copy' + (i > 1 ? (' ' + i) : '') + validName = `${strippedName} (${copy})` + i++ + } + + return validName +} diff --git a/test/util.test.js b/test/util.test.js index 4a32737..d46b6ef 100644 --- a/test/util.test.js +++ b/test/util.test.js @@ -224,5 +224,51 @@ describe('util', function () { assert.strictEqual(util.makeFieldTooltip({examples: ['foo']}, 'pt-BR'), 'Exemplos\n"foo"'); }); }); + + it('should find a unique name', function () { + assert.strictEqual(util.findUniqueName('other', [ + 'a', + 'b', + 'c' + ]), 'other') + + assert.strictEqual(util.findUniqueName('b', [ + 'a', + 'b', + 'c' + ]), 'b (copy)') + + assert.strictEqual(util.findUniqueName('b', [ + 'a', + 'b', + 'c', + 'b (copy)' + ]), 'b (copy 2)') + + assert.strictEqual(util.findUniqueName('b', [ + 'a', + 'b', + 'c', + 'b (copy)', + 'b (copy 2)' + ]), 'b (copy 3)') + + assert.strictEqual(util.findUniqueName('b (copy)', [ + 'a', + 'b', + 'b (copy)', + 'b (copy 2)', + 'c' + ]), 'b (copy 3)') + + assert.strictEqual(util.findUniqueName('b (copy 2)', [ + 'a', + 'b', + 'b (copy)', + 'b (copy 2)', + 'c' + ]), 'b (copy 3)') + }) + // TODO: thoroughly test all util methods }); \ No newline at end of file From d31917390c0e36605fd1c18c8893e69b445d4541 Mon Sep 17 00:00:00 2001 From: jos Date: Sat, 30 Mar 2019 13:47:40 +0100 Subject: [PATCH 2/7] Revert to previous valid value on blur --- src/js/Node.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/js/Node.js b/src/js/Node.js index e7c8a0a..81d9f4d 100644 --- a/src/js/Node.js +++ b/src/js/Node.js @@ -1567,11 +1567,7 @@ Node.prototype._getDomValue = function(silent) { } } catch (err) { - this.value = undefined; - // TODO: sent an action with the new, invalid value? - if (silent !== true) { - throw err; - } + // keep the previous (valid) value } } }; @@ -1913,11 +1909,7 @@ Node.prototype._getDomField = function(silent) { } } catch (err) { - this.field = undefined; - // TODO: sent an action here, with the new, invalid value? - if (silent !== true) { - throw err; - } + // keep the previous (valid) field value } } }; From 6ccf5c651d588b03ce151d7240067a0e82c783ed Mon Sep 17 00:00:00 2001 From: jos Date: Sat, 30 Mar 2019 14:13:25 +0100 Subject: [PATCH 3/7] Fix redo duplicate not checking for duplicate property names --- src/js/History.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/js/History.js b/src/js/History.js index 5bccbb2..6f70424 100644 --- a/src/js/History.js +++ b/src/js/History.js @@ -1,5 +1,7 @@ 'use strict'; +var util = require('./util'); + /** * @constructor History * Store action history, enables undo and redo @@ -121,6 +123,8 @@ function History (editor) { var nodes = params.paths.map(findNode); nodes.forEach(function (node) { var clone = node.clone(); + var existingFieldNames = parentNode.getFieldNames(); + clone.field = util.findUniqueName(node.field, existingFieldNames); parentNode.insertAfter(clone, afterNode); afterNode = clone; }); From 19a625fa6ed4042dae1ff867ee165d9fe996b2d4 Mon Sep 17 00:00:00 2001 From: jos Date: Sat, 30 Mar 2019 14:15:24 +0100 Subject: [PATCH 4/7] Fix duplicate not working for arrays anymore --- src/js/History.js | 6 ++++-- src/js/Node.js | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/js/History.js b/src/js/History.js index 6f70424..ac0da93 100644 --- a/src/js/History.js +++ b/src/js/History.js @@ -123,8 +123,10 @@ function History (editor) { var nodes = params.paths.map(findNode); nodes.forEach(function (node) { var clone = node.clone(); - var existingFieldNames = parentNode.getFieldNames(); - clone.field = util.findUniqueName(node.field, existingFieldNames); + if (parentNode.type === 'object') { + var existingFieldNames = parentNode.getFieldNames(); + clone.field = util.findUniqueName(node.field, existingFieldNames); + } parentNode.insertAfter(clone, afterNode); afterNode = clone; }); diff --git a/src/js/Node.js b/src/js/Node.js index 81d9f4d..7752678 100644 --- a/src/js/Node.js +++ b/src/js/Node.js @@ -3508,8 +3508,10 @@ Node.onDuplicate = function(nodes) { var afterNode = lastNode; var clones = nodes.map(function (node) { var clone = node.clone(); - var existingFieldNames = node.parent.getFieldNames(); - clone.field = util.findUniqueName(node.field, existingFieldNames); + if (node.parent.type === 'object') { + var existingFieldNames = node.parent.getFieldNames(); + clone.field = util.findUniqueName(node.field, existingFieldNames); + } parent.insertAfter(clone, afterNode); afterNode = clone; return clone; From 12d908d08a29d14bd9b06e51962a4d38dab04c54 Mon Sep 17 00:00:00 2001 From: jos Date: Sat, 30 Mar 2019 20:58:43 +0100 Subject: [PATCH 5/7] Show warning icon when value or field is invalid or duplicate --- src/js/Node.js | 121 +++++++++++++++++++++++++++++++++---------------- src/js/i18n.js | 12 +++++ 2 files changed, 95 insertions(+), 38 deletions(-) diff --git a/src/js/Node.js b/src/js/Node.js index 7752678..a001e8b 100644 --- a/src/js/Node.js +++ b/src/js/Node.js @@ -268,7 +268,7 @@ Node.prototype.setError = function (error, child) { * Render the error */ Node.prototype.updateError = function() { - var error = this.error; + var error = this.fieldError || this.valueError || this.error; var tdError = this.dom.tdError; if (error && this.dom && this.dom.tr) { util.addClassName(this.dom.tr, 'jsoneditor-validation-error'); @@ -379,7 +379,7 @@ Node.prototype.setField = function(field, fieldEditable) { */ Node.prototype.getField = function() { if (this.field === undefined) { - this._getDomField(false); + this._getDomField(true); } return this.field; @@ -920,23 +920,23 @@ Node.prototype.hideChilds = function(options) { */ Node.prototype._updateCssClassName = function () { if(this.dom.field - && this.editor - && this.editor.options + && this.editor + && this.editor.options && typeof this.editor.options.onClassName ==='function' - && this.dom.tree){ - util.removeAllClassNames(this.dom.tree); + && this.dom.tree){ + util.removeAllClassNames(this.dom.tree); var addClasses = this.editor.options.onClassName({ path: this.getPath(), field: this.field, value: this.value }) || ""; util.addClassName(this.dom.tree, "jsoneditor-values " + addClasses); } }; -Node.prototype.recursivelyUpdateCssClassesOnNodes = function () { +Node.prototype.recursivelyUpdateCssClassesOnNodes = function () { this._updateCssClassName(); if (Array.isArray(this.childs)) { for (var i = 0; i < this.childs.length; i++) { this.childs[i].recursivelyUpdateCssClassesOnNodes(); } - } + } } /** @@ -1317,8 +1317,8 @@ Node.select = function(editableDiv) { */ Node.prototype.blur = function() { // retrieve the actual field and value from the DOM. - this._getDomValue(false); - this._getDomField(false); + this._getDomValue(); + this._getDomField(true); }; /** @@ -1541,11 +1541,11 @@ Node.prototype.deepEqual = function (json) { /** * Retrieve value from DOM - * @param {boolean} [silent] If true (default), no errors will be thrown in - * case of invalid data * @private */ -Node.prototype._getDomValue = function(silent) { +Node.prototype._getDomValue = function() { + this._clearValueError(); + if (this.dom.value && this.type != 'array' && this.type != 'object') { this.valueInnerText = util.getInnerText(this.dom.value); } @@ -1567,11 +1567,50 @@ Node.prototype._getDomValue = function(silent) { } } catch (err) { - // keep the previous (valid) value + // keep the previous value + this._setValueError(translate('cannotParseValueError')); } } }; +/** + * Show a local error in case of invalid value + * @param {string} message + * @private + */ +Node.prototype._setValueError = function (message) { + this.valueError = { + message: message + }; + this.updateError(); +} + +Node.prototype._clearValueError = function () { + if (this.valueError) { + this.valueError = null; + this.updateError(); + } +} + +/** + * Show a local error in case of invalid or duplicate field + * @param {string} message + * @private + */ +Node.prototype._setFieldError = function (message) { + this.fieldError = { + message: message + }; + this.updateError(); +} + +Node.prototype._clearFieldError = function () { + if (this.fieldError) { + this.fieldError = null; + this.updateError(); + } +} + /** * Handle a changed value * @private @@ -1878,11 +1917,13 @@ Node.prototype._updateDomField = function () { /** * Retrieve field from DOM - * @param {boolean} [silent] If true (default), no errors will be thrown in - * case of invalid data + * @param {boolean} [forceUnique] If true, the field name will be changed + * into a unique name in case it is a duplicate. * @private */ -Node.prototype._getDomField = function(silent) { +Node.prototype._getDomField = function(forceUnique) { + this._clearFieldError(); + if (this.dom.field && this.fieldEditable) { this.fieldInnerText = util.getInnerText(this.dom.field); } @@ -1900,16 +1941,25 @@ Node.prototype._getDomField = function(silent) { this._debouncedOnChangeField(); } else { - if (!silent) { + if (forceUnique) { // fix duplicate field: change it into a unique name - this.field = util.findUniqueName(field, existingFieldNames); - this._debouncedOnChangeField(); + field = util.findUniqueName(field, existingFieldNames); + if (field !== this.field) { + this.field = field; + // this._debouncedOnChangeField = util.debounce(this._onChangeField.bind(this), Node.prototype.DEBOUNCE_INTERVAL); + // this._onChangeField(); + this._debouncedOnChangeField(); //FIXME: don't debounce but resolve right away, and cancel current debounce + } + } + else { + this._setFieldError(translate('duplicateFieldError')); } } } } catch (err) { - // keep the previous (valid) field value + // keep the previous field value + this._setFieldError(translate('cannotParseFieldError')); } } }; @@ -2412,6 +2462,7 @@ Node.prototype.setSelected = function (selected, isFirst) { Node.prototype.updateValue = function (value) { this.value = value; this.previousValue = value; + this.valueError = undefined; this.updateDom(); }; @@ -2422,6 +2473,7 @@ Node.prototype.updateValue = function (value) { Node.prototype.updateField = function (field) { this.field = field; this.previousField = field; + this.fieldError = undefined; this.updateDom(); }; @@ -2495,7 +2547,7 @@ Node.prototype.updateDom = function (options) { // update field and value this._updateDomField(); this._updateDomValue(); - + this._updateCssClassName(); // update childs indexes @@ -2599,7 +2651,7 @@ Node._findSchema = function (schema, schemaRefs, path) { foundSchema = Node._findSchema(childSchema, schemaRefs, path); } } - + for (var i = 0; i < path.length && childSchema; i++) { var nextPath = path.slice(i + 1, path.length); var key = path[i]; @@ -2864,7 +2916,8 @@ Node.prototype.onEvent = function (event) { switch (type) { case 'blur': case 'change': - this._getDomValue(true); + this._getDomValue(); + this._clearValueError(); this._updateDomValue(); if (this.value) { domValue.innerHTML = this._escapeHTML(this.value); @@ -2873,7 +2926,7 @@ Node.prototype.onEvent = function (event) { case 'input': //this._debouncedGetDomValue(true); // TODO - this._getDomValue(true); + this._getDomValue(); this._updateDomValue(); break; @@ -2895,14 +2948,14 @@ Node.prototype.onEvent = function (event) { case 'keyup': //this._debouncedGetDomValue(true); // TODO - this._getDomValue(true); + this._getDomValue(); this._updateDomValue(); break; case 'cut': case 'paste': setTimeout(function () { - node._getDomValue(true); + node._getDomValue(); node._updateDomValue(); }, 1); break; @@ -2914,14 +2967,6 @@ Node.prototype.onEvent = function (event) { if (target == domField) { switch (type) { case 'blur': - this._getDomField(false); - this._updateDomField(); - if (this.field) { - domField.innerHTML = this._escapeHTML(this.field); - } - break; - - case 'change': this._getDomField(true); this._updateDomField(); if (this.field) { @@ -2930,7 +2975,7 @@ Node.prototype.onEvent = function (event) { break; case 'input': - this._getDomField(true); + this._getDomField(); this._updateSchema(); this._updateDomField(); this._updateDomValue(); @@ -2942,14 +2987,14 @@ Node.prototype.onEvent = function (event) { break; case 'keyup': - this._getDomField(true); + this._getDomField(); this._updateDomField(); break; case 'cut': case 'paste': setTimeout(function () { - node._getDomField(true); + node._getDomField(); node._updateDomField(); }, 1); break; diff --git a/src/js/i18n.js b/src/js/i18n.js index 9c4450a..318e6e7 100644 --- a/src/js/i18n.js +++ b/src/js/i18n.js @@ -22,6 +22,9 @@ var _defs = { duplicateText: 'Duplicate', duplicateTitle: 'Duplicate selected fields (Ctrl+D)', duplicateField: 'Duplicate this field (Ctrl+D)', + duplicateFieldError: 'Duplicate field name', + cannotParseFieldError: 'Cannot parse field into JSON', + cannotParseValueError: 'Cannot parse value into JSON', empty: 'empty', expandAll: 'Expand all fields', expandTitle: 'Click to expand/collapse this field (Ctrl+E). \n' + @@ -106,6 +109,9 @@ var _defs = { duplicateText: '复制', duplicateTitle: '复制选中字段(Ctrl+D)', duplicateField: '复制该字段(Ctrl+D)', + duplicateFieldError: '重复的字段名称', + cannotParseFieldError: '无法将字段解析为JSON', + cannotParseValueError: '无法将值解析为JSON', empty: '清空', expandAll: '展开所有字段', expandTitle: '点击 展开/收缩 该字段(Ctrl+E). \n' + @@ -190,6 +196,9 @@ var _defs = { duplicateText: 'Duplicar', duplicateTitle: 'Duplicar campos selecionados (Ctrl+D)', duplicateField: 'Duplicar este campo (Ctrl+D)', + duplicateFieldError: 'Nome do campo duplicado', + cannotParseFieldError: 'Não é possível analisar o campo no JSON', + cannotParseValueError: 'Não é possível analisar o valor em JSON', empty: 'vazio', expandAll: 'Expandir todos campos', expandTitle: 'Clique para expandir/encolher este campo (Ctrl+E). \n' + @@ -286,6 +295,9 @@ var _defs = { duplicateText: 'Aşağıya kopyala', duplicateTitle: 'Seçili alanlardan bir daha oluştur (Ctrl+D)', duplicateField: 'Bu alandan bir daha oluştur (Ctrl+D)', + duplicateFieldError: 'Duplicate field name', + cannotParseFieldError: 'Alan JSON\'a ayrıştırılamıyor', + cannotParseValueError: 'JSON\'a değer ayrıştırılamıyor', empty: 'boş', expandAll: 'Tüm alanları aç', expandTitle: 'Bu alanı açmak/kapatmak için tıkla (Ctrl+E). \n' + From 144be4a16d369b4a3f3812804593e4a54fca307f Mon Sep 17 00:00:00 2001 From: jos Date: Sun, 31 Mar 2019 16:29:09 +0200 Subject: [PATCH 6/7] Fix duplicate warning icon not aways showing up --- src/js/Node.js | 45 ++++++++++++++++++--------------------------- src/js/treemode.js | 8 +------- 2 files changed, 19 insertions(+), 34 deletions(-) diff --git a/src/js/Node.js b/src/js/Node.js index a001e8b..93c4875 100644 --- a/src/js/Node.js +++ b/src/js/Node.js @@ -379,7 +379,7 @@ Node.prototype.setField = function(field, fieldEditable) { */ Node.prototype.getField = function() { if (this.field === undefined) { - this._getDomField(true); + this._getDomField(); } return this.field; @@ -1312,15 +1312,6 @@ Node.select = function(editableDiv) { }, 0); }; -/** - * Update the values from the DOM field and value of this node - */ -Node.prototype.blur = function() { - // retrieve the actual field and value from the DOM. - this._getDomValue(); - this._getDomField(true); -}; - /** * Check if given node is a child. The method will check recursively to find * this node. @@ -1932,28 +1923,28 @@ Node.prototype._getDomField = function(forceUnique) { try { var field = this._unescapeHTML(this.fieldInnerText); - if (field !== this.field) { - var existingFieldNames = this.parent.getFieldNames(this); - var isDuplicate = existingFieldNames.indexOf(field) !== -1; + var existingFieldNames = this.parent.getFieldNames(this); + var isDuplicate = existingFieldNames.indexOf(field) !== -1; - if (!isDuplicate) { + if (!isDuplicate) { + if (field !== this.field) { this.field = field; this._debouncedOnChangeField(); } + } + else { + if (forceUnique) { + // fix duplicate field: change it into a unique name + field = util.findUniqueName(field, existingFieldNames); + if (field !== this.field) { + this.field = field; + + // TODO: don't debounce but resolve right away, and cancel current debounce + this._debouncedOnChangeField(); + } + } else { - if (forceUnique) { - // fix duplicate field: change it into a unique name - field = util.findUniqueName(field, existingFieldNames); - if (field !== this.field) { - this.field = field; - // this._debouncedOnChangeField = util.debounce(this._onChangeField.bind(this), Node.prototype.DEBOUNCE_INTERVAL); - // this._onChangeField(); - this._debouncedOnChangeField(); //FIXME: don't debounce but resolve right away, and cancel current debounce - } - } - else { - this._setFieldError(translate('duplicateFieldError')); - } + this._setFieldError(translate('duplicateFieldError')); } } } diff --git a/src/js/treemode.js b/src/js/treemode.js index 10f73b4..35ff43d 100644 --- a/src/js/treemode.js +++ b/src/js/treemode.js @@ -283,13 +283,7 @@ treemode.update = function (json) { * @return {Object | undefined} json */ treemode.get = function () { - // remove focus from currently edited node - if (this.focusTarget) { - var node = Node.getNodeFromTarget(this.focusTarget); - if (node) { - node.blur(); - } - } + // TODO: resolve pending debounced input changes if any, but do not resolve invalid inputs if (this.node) { return this.node.getValue(); From 7624919a6c7c2fe03a34c335596d94bf5c372085 Mon Sep 17 00:00:00 2001 From: jos Date: Tue, 2 Apr 2019 20:47:29 +0200 Subject: [PATCH 7/7] Fix broken build (remove usage of ` characters) --- src/js/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/util.js b/src/js/util.js index 6bb4c94..9828e6d 100644 --- a/src/js/util.js +++ b/src/js/util.js @@ -1164,7 +1164,7 @@ exports.findUniqueName = function(name, existingPropNames) { while (existingPropNames.indexOf(validName) !== -1) { var copy = 'copy' + (i > 1 ? (' ' + i) : '') - validName = `${strippedName} (${copy})` + validName = strippedName + ' (' + copy + ')' i++ }