'use strict'; var escapeHTML = require('../Utils').escapeHTML; var cmdHelper = require('../helper/CmdHelper'); var domQuery = require('min-dom').query, domAttr = require('min-dom').attr, domClosest = require('min-dom').closest; var filter = require('lodash/filter'), forEach = require('lodash/forEach'), keys = require('lodash/keys'); var domify = require('min-dom').domify; var entryFieldDescription = require('./EntryFieldDescription'); var updateSelection = require('selection-update'); var TABLE_ROW_DIV_SNIPPET = '
'; var DELETE_ROW_BUTTON_SNIPPET = ''; function createInputRowTemplate(properties, canRemove) { var template = TABLE_ROW_DIV_SNIPPET; template += createInputTemplate(properties, canRemove); template += canRemove ? DELETE_ROW_BUTTON_SNIPPET : ''; template += '
'; return template; } function createInputTemplate(properties, canRemove) { var columns = properties.length; var template = ''; forEach(properties, function(prop) { template += ''; }); return template; } function createLabelRowTemplate(labels) { var template = TABLE_ROW_DIV_SNIPPET; template += createLabelTemplate(labels); template += ''; return template; } function createLabelTemplate(labels) { var columns = labels.length; var template = ''; forEach(labels, function(label) { template += ''; }); return template; } function pick(elements, properties) { return (elements || []).map(function(elem) { var newElement = {}; forEach(properties, function(prop) { newElement[prop] = elem[prop] || ''; }); return newElement; }); } function diff(element, node, values, oldValues, editable) { return filter(values, function(value, idx) { return !valueEqual(element, node, value, oldValues[idx], editable, idx); }); } function valueEqual(element, node, value, oldValue, editable, idx) { if (value && !oldValue) { return false; } var allKeys = keys(value).concat(keys(oldValue)); return allKeys.every(function(key) { var n = value[key] || undefined; var o = oldValue[key] || undefined; return !editable(element, node, key, idx) || n === o; }); } function getEntryNode(node) { return domClosest(node, '[data-entry]', true); } function getContainer(node) { return domQuery('div[data-list-entry-container]', node); } function getSelection(node) { return { start: node.selectionStart, end: node.selectionEnd }; } function setSelection(node, selection) { node.selectionStart = selection.start; node.selectionEnd = selection.end; } /** * @param {Object} options * @param {string} options.id * @param {string} options.description * @param {Array} options.modelProperties * @param {Array} options.labels * @param {Function} options.getElements - this callback function must return a list of business object items * @param {Function} options.removeElement * @param {Function} options.addElement * @param {Function} options.updateElement * @param {Function} options.editable * @param {Function} options.setControlValue * @param {Function} options.show * * @return {Object} */ module.exports = function(options) { var id = options.id, modelProperties = options.modelProperties, labels = options.labels, description = options.description; var labelRow = createLabelRowTemplate(labels); var getElements = options.getElements; var removeElement = options.removeElement, canRemove = typeof removeElement === 'function'; var addElement = options.addElement, canAdd = typeof addElement === 'function', addLabel = options.addLabel || 'Add Value'; var updateElement = options.updateElement, canUpdate = typeof updateElement === 'function'; var editable = options.editable || function() { return true; }, setControlValue = options.setControlValue; var show = options.show, canBeShown = typeof show === 'function'; var elements = function(element, node) { return pick(getElements(element, node), modelProperties); }; var factory = { id: id, html: (canAdd ? '
' + '' + '' + '
' : '') + '
' + '
' + labelRow + '
' + '
' + '
' + '
' + // add description below table entry field (description ? entryFieldDescription(description) : ''), get: function(element, node) { var boElements = elements(element, node, this.__invalidValues); var invalidValues = this.__invalidValues; delete this.__invalidValues; forEach(invalidValues, function(value, idx) { var element = boElements[idx]; forEach(modelProperties, function(prop) { element[prop] = value[prop]; }); }); return boElements; }, set: function(element, values, node) { var action = this.__action || {}; delete this.__action; if (action.id === 'delete-element') { return removeElement(element, node, action.idx); } else if (action.id === 'add-element') { return addElement(element, node); } else if (canUpdate) { var commands = [], valuesToValidate = values; if (typeof options.validate !== 'function') { valuesToValidate = diff(element, node, values, elements(element, node), editable); } var self = this; forEach(valuesToValidate, function(value) { var validationError, idx = values.indexOf(value); if (typeof options.validate === 'function') { validationError = options.validate(element, value, node, idx); } if (!validationError) { var cmd = updateElement(element, value, node, idx); if (cmd) { commands.push(cmd); } } else { // cache invalid value in an object by index as key self.__invalidValues = self.__invalidValues || {}; self.__invalidValues[idx] = value; // execute a command, which does not do anything commands.push(cmdHelper.updateProperties(element, {})); } }); return commands; } }, createListEntryTemplate: function(value, index, selectBox) { return createInputRowTemplate(modelProperties, canRemove); }, addElement: function(element, node, event, scopeNode) { var template = domify(createInputRowTemplate(modelProperties, canRemove)); var container = getContainer(node); container.appendChild(template); this.__action = { id: 'add-element' }; return true; }, deleteElement: function(element, node, event, scopeNode) { var container = getContainer(node); var rowToDelete = event.delegateTarget.parentNode; var idx = parseInt(domAttr(rowToDelete, 'data-index'), 10); container.removeChild(rowToDelete); this.__action = { id: 'delete-element', idx: idx }; return true; }, editable: function(element, rowNode, input, prop, value, idx) { var entryNode = domClosest(rowNode, '[data-entry]'); return editable(element, entryNode, prop, idx); }, show: function(element, entryNode, node, scopeNode) { entryNode = getEntryNode(entryNode); return show(element, entryNode, node, scopeNode); }, showTable: function(element, entryNode, node, scopeNode) { entryNode = getEntryNode(entryNode); var elems = elements(element, entryNode); return elems && elems.length && (!canBeShown || show(element, entryNode, node, scopeNode)); }, validateListItem: function(element, value, node, idx) { if (typeof options.validate === 'function') { return options.validate(element, value, node, idx); } } }; // Update/set the selection on the correct position. // It's the same code like for an input value in the PropertiesPanel.js. if (setControlValue) { factory.setControlValue = function(element, rowNode, input, prop, value, idx) { var entryNode = getEntryNode(rowNode); var isReadOnly = domAttr(input, 'readonly'); var oldValue = input.value; var selection; // prevents input fields from having the value 'undefined' if (value === undefined) { value = ''; } // when the attribute 'readonly' exists, ignore the comparison // with 'oldValue' and 'value' if (!!isReadOnly && oldValue === value) { return; } // update selection on undo/redo if (document.activeElement === input) { selection = updateSelection(getSelection(input), oldValue, value); } setControlValue(element, entryNode, input, prop, value, idx); if (selection) { setSelection(input, selection); } }; } return factory; };