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