closedquestion-8.x-3.x-dev/assets/js/xmlEditor.js

assets/js/xmlEditor.js
/**
 * @file
 * The ClosedQuestion XML editor.
 *
 * @param {object} jQuery
 * @param {object} Drupal
 * @require
 *   Functions found in xmlQuestionConvert.js.
 */
(function (jQuery, Drupal) {
  jQuery.fn.xmlTreeEditor = function () {

    if (typeof jQuery.fn.on === 'undefined') {
      /* fix for jQuery <1.7 */
      jQuery.fn.on = function (type, data, fn) {
        // Handle object literals
        if (typeof type === "object") {
          for (var key in type) {
            this[ name ](key, data, type[key], fn);
          }
          return this;
        }

        if (jQuery.isFunction(data) || data === false) {
          fn = data;
          data = undefined;
        }

        var handler = name === "one" ? jQuery.proxy(fn, function (event) {
          jQuery(this).unbind(event, handler);
          return fn.apply(this, arguments);
        }) : fn;

        if (type === "unload" && name !== "one") {
          this.one(type, data, fn);

        }
        else {
          for (var i = 0, l = this.length; i < l; i++) {
            jQuery.event.add(this[i], type, handler, data);
          }
        }

        return this;
      };
    }

    // https://tc39.github.io/ecma262/#sec-array.prototype.find
    if (!Array.prototype.find) {
      Object.defineProperty(Array.prototype, 'find', {
        value: function (predicate) {
          // 1. Let O be ? ToObject(this value).
          if (this == null) {
            throw new TypeError('"this" is null or not defined');
          }

          var o = Object(this);

          // 2. Let len be ? ToLength(? Get(O, "length")).
          var len = o.length >>> 0;

          // 3. If IsCallable(predicate) is false, throw a TypeError exception.
          if (typeof predicate !== 'function') {
            throw new TypeError('predicate must be a function');
          }

          // 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
          var thisArg = arguments[1];

          // 5. Let k be 0.
          var k = 0;

          // 6. Repeat, while k < len
          while (k < len) {
            // a. Let Pk be ! ToString(k).
            // b. Let kValue be ? Get(O, Pk).
            // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
            // d. If testResult is true, return kValue.
            var kValue = o[k];
            if (predicate.call(thisArg, kValue, k, o)) {
              return kValue;
            }
            // e. Increase k by 1.
            k++;
          }

          // 7. Return undefined.
          return undefined;
        }
      });
    }



    /**
     * The data of the current instance of the editor.
     */
    var data = this.data();
    /**
     * The configuration object. Private: use function getConfig()
     */
    var _config = data.xte_config;


    /**
     * This will hold the JQuery object of the tree container
     */
    var $xmlJsonEditor_tree_container = jQuery('#xmlJsonEditor_tree_container');

    /**
     * The jQuery selector used to get the div that shows the editor controls.
     */
    var editorSelector = data.xte_editor;
    /**
     * Listeners to global events.
     */
    var listeners = data.listeners;

    /**
     * saveHandlers are specific to the "currently shown attribute editors" and
     * are thus cleared out when a new node is selected for editing.
     */
    var saveHandlers = data.saveHandlers;
    /**
     * A numeric id used to generate unique id's in this tree.
     */
    var nextId = data.nextId;
    /**
     * The (jQuery enhanced) tree.
     */
    var tree = this;
    if (saveHandlers === undefined) {
      saveHandlers = [];
    }

    switch (arguments[0]) {
      case 'init':
        // Clear the listeners.
        listeners = {
          change: [],
          onloadeditor: [],
          init: []
        };
        nextId = 1;
        editorSelector = arguments[1];
        var xmlString = jQuery.trim(arguments[2]);
        _config = arguments[3];
        initEditor(editorSelector);
        doInit(tree, xmlString, getConfig());

        /* make sure data is saved if user mouse is leaving the editor area */
        if (jQuery('#xmlJsonEditor_container').data('xmleditor.mouseleave') !== true) {
          jQuery('#xmlJsonEditor_container').on('mouseleave', function () {
            var bodyField = CQ_FindBodyField(drupalSettings.closedquestion.language);
            bodyField.value = $xmlJsonEditor_tree_container.xmlTreeEditor('read');
            jQuery('#xmlJsonEditor_container').data('xmleditor.mouseleave', true);
          });
        }
        jQuery('#xmlJsonEditor_tree_container').attr('data-xmleditor-is-init', 'true');
        break;
      case 'read':

        return treeToXml(tree);
        break;
      case 'select':
        if (jQuery('#selectedNodeUpdateButton').data('isEdited') === true) {
          if (!confirm('You have unsaved changes, continue?')) {
            break;
          }
          jQuery('#selectedNodeUpdateButton').data('isEdited', false);
        }
        var $element = arguments[1];
        if ($element[0].nodeName.toLowerCase() === 'ul') {
          /* event triggered by removal of other element, find currently selected item in tree */
          $element = tree.find('a.jstree_cq-clicked').closest('li');
        }
        createEditorFor($element);
        break;
      case 'addNode':
        if (!arguments[1]) {
          addNode();
        }
        else {
          var addType = arguments[1].toString();
          var parent = arguments[2];
          var attributes = arguments[3];
          addNode(addType, parent, attributes);
        }
        break;
      case 'delNode':
        removeSelectedNode();
        break;
      case 'saveNode':
        updateSelectedNode();
        break;
      case 'cloneNode':
        cloneSelectedNode();
        break;
      case 'search':
        return _search(arguments);
        break;
      case 'closest':
        return _closest(arguments);
        break;
      case 'bind':
        var bindType = arguments[1].toLowerCase();
        var listener = arguments[2];
        if (listeners && listeners[bindType] !== undefined) {
          listeners[bindType].push(listener);
        }
        break;
    }

    data.xte_config = _config;
    data.xte_editor = editorSelector;
    data.listeners = listeners;
    data.saveHandlers = saveHandlers;
    data.nextId = nextId;
    this.data(data);

    /**
     * Initialises a tree from data.
     *
     * @param {object} treeContainer
     * @param {string} xmlString
     * @param {object} config
     */
    function doInit(treeContainer, xmlString, config) {
      var treeData;
      var attName;
      if (xmlString.length === 0) {
        var attributes = {};
        var rootName = config.valid_children[0];
        var rootConfig = config.types[rootName];
        if (rootConfig.attributes !== undefined) {
          checkMandatoryAttributes(attributes, rootConfig.attributes);
        }
        xmlString = "<" + config.valid_children[0];
        for (attName in attributes) {
          xmlString += " " + attName + '="' + attributes[attName] + '"';
        }
        xmlString += "/>";
      }

      treeData = questionStringToTreeObject(xmlString);
      for (var key in config.types) {
        config.types[key].icon = !config.types[key].icon ? {} : config.types[key].icon;
        config.types[key].icon.image = (!config.types[key].icon.image) ? 'icons/mapping_icon.png' : config.types[key].icon.image;

        if (config.types[key].icon !== undefined) {
          if (config.types[key].icon.image.substring(0, 1) !== '/') {
            config.types[key].icon.image = config.basePath + '/assets/' + config.types[key].icon.image;
          }
        }
      }
      var treeConfig = {
        "core": {
          "html_titles": true
        },
        "plugins": ["themes", "json_data", "ui", "types", "dnd", "crrm", "search"],
        "json_data": {},
        "ui": {
          "select_limit": "1"
        },
        "themes": {
          "theme": "classic"
        },
        "search": {},
        "types": config
      };
      treeConfig.json_data.data = treeData;
      jQuery(treeContainer).jstree_cq(treeConfig);
      treeContainer.bind("select_node.jstree_cq",
        function (tree) {
          return function (e, args) {
            var element = jQuery(args.args[0]).parent();
            if (element.length > 0) {
              //jQuery("#log1").html("Last operation: " + e.type);
              tree.xmlTreeEditor('select', element);
            }
          };
        }(tree));

      // Set class of tree items.
      jQuery(treeContainer).find('li').each(function () {
        _setTreeElementClass(jQuery(this));
      });

      updateTreeItemsGui();

      /* enable F8 for saving */
      if (jQuery('#edit-submit').data('xmleditor.f8_init') !== true) {
        jQuery('#edit-submit').data('xmleditor.f8_init', true);
        jQuery('#edit-submit').val(jQuery('#edit-submit').val() + ' (F8)');
        jQuery(window).on('keydown', function (e) {
          if (e.which === 119) {
            jQuery('#edit-submit').click();
          }
        });
      }

      /* enable add buttons */
      jQuery('#selectedNodeRemoveButton').unbind('click').bind('click', function () {
        tree.xmlTreeEditor('delNode');
      });
      jQuery('#selectedNodeCloneButton').unbind('click').bind('click', function () {
        tree.xmlTreeEditor('cloneNode');
      });
      jQuery('#selectedNodeUpdateButton').unbind('click').bind('click', function () {
        jQuery('#selectedNodeUpdateButton').data('isEdited', false);
        tree.xmlTreeEditor('saveNode');
      });

      jQuery('#xmlJsonEditorTable').colResizable({disable: true}); // Remove previous
      jQuery('#xmlJsonEditorTable').colResizable();
      notifyListeners('init', treeData);
    }

    /**
     * Helper function. Gives tree element the same class as the rel attribute.
     *  This so we can more efficiently select it.
     *
     * @param {object} $element
     *   jQuerified tree element.
     */
    function _setTreeElementClass($element) {
      var relAttrVal = $element.attr('rel');
      if (relAttrVal) {
        $element.addClass('xmlJsonEditor_' + relAttrVal);
      }
    }

    /**
     * Sets icon, title of tree items.
     *
     * @returns {undefined}
     */
    function updateTreeItemsGui() {
      jQuery(tree).find('li').each(function () {
        var $element = jQuery(this);
        var elementConfig = getConfig($element);
        setElementTreeIcon($element, elementConfig);
        tree.jstree_cq("core").set_text($element, createTitleForElement($element, elementConfig));
      });
    }

    /**
     * Reads out a tree and converts it back to XML
     * @param {object} treeContainer
     */
    function treeToXml(treeContainer) {
      var xmlString = '';
      if (treeContainer && treeContainer.jstree_cq && treeContainer.jstree_cq("core") && treeContainer.jstree_cq("core").get_container) {
        var container = treeContainer.jstree_cq("core").get_container();
        var root = container[0].children[0].children[0];
        var treeRoot = treeContainer.jstree_cq("core")._get_node(root);
        var xmlDoc;
        if (document.implementation && document.implementation.createDocument) {
          xmlDoc = document.implementation.createDocument("", "", null);
        }
        else {
          xmlDoc = new ActiveXObject("MSXML2.DOMDocument");
        }
        if (treeRoot.length > 0) {
          addNodeToXML(treeContainer, xmlDoc, xmlDoc, treeRoot);
        }

        xmlString = getSerializedXML(xmlDoc);
      }

      return htmlToXHTML(xmlString);
    }

    /**
     * Converts html to XHTML
     *
     * @param {string} html
     * @param {array} special_element_definitions
     *  See $wysiwygEditor.data('xmleditor_element_definition');
     * @returns {string}
     */
    function htmlToXHTML(html, special_element_definitions) {
      // Convert self closing elements (e.g. <mathresult></mathresult> to self closing tags <mathresult />.
      if (special_element_definitions) {
        jQuery.each(special_element_definitions, function (i, element_def) {
          if (element_def.selfClosing) {
            html = html.replace(new RegExp('(<\s*' + element_def.tagName + '[^>]*)>(.*?)<\s*/\s*' + element_def.tagName + '>', 'g'), '$1/>');
          }
        });
      }

      html = html.replace(/<br>/g, '<br />');
      html = html.replace(/(<img[^>]+)(>)/g, '$1/>'); // prevent <img...>
      html = html.replace(/(<img[^>]+)\/\/(>)/g, '$1/>'); // prevent <img..//>

      html = html.replace(/&nbsp;/g, ' ');

      return html;
    }

    /**
     * Helper function to quickly determine whether a node is a group node.
     *
     * @param {mixed} nodeReference
     * @returns {mixed}
     */
    function nodeIsGroup(nodeReference) {
      var nodeType = typeof nodeReference === 'string' ? nodeReference : jQuery(nodeReference).data().jstree_cq.type;
      var config = getConfig();
      return config.types[nodeType].is_group;
    }

    /**
     * @param treeContainer
     *   The jQuery enhanced DOM object that contains the jstree_cq.
     * @param xmlDoc
     *   The xml document to add the node to.
     * @param parent
     *   xml element to use as the parent for the new node.
     * @param node
     *   jstree_cq tree node (jQuery extended li element) to use as basis for the
     *   new node.
     */
    function addNodeToXML(treeContainer, xmlDoc, parent, node) {
      var data = node.data().jstree_cq;
      var children;
      var child;
      var n;
      if (nodeIsGroup(node) === undefined) {
        var xmlNode = xmlDoc.createElement(data.type);
        for (var attName in data.attributes) {
          var attValue = data.attributes[attName];
          xmlNode.setAttribute(attName, attValue);
        }
        if (data.content.length > 0) {
          InnerHTMLToNode(data.content, xmlNode);
        }
        parent.appendChild(xmlNode);
        children = treeContainer.jstree_cq("core")._get_children(node);
        for (n = 0; n < children.length; n++) {
          child = treeContainer.jstree_cq("core")._get_node(children[n]);
          addNodeToXML(treeContainer, xmlDoc, xmlNode, child);
        }
      }
      else {
        children = treeContainer.jstree_cq("core")._get_children(node);
        for (n = 0; n < children.length; n++) {
          child = treeContainer.jstree_cq("core")._get_node(children[n]);
          addNodeToXML(treeContainer, xmlDoc, parent, child);
        }
      }
    }

    /**
     * Creates the DOM elements for the editor.
     *
     * @param {string} editorSelector
     */
    function initEditor(editorSelector) {
      jQuery(editorSelector).empty().append('\n\n\
      <div id=""></div>\n\
      <fieldset id="editor_values"> \
           \n\
            <form> \n\
            <table class = "editor_values_contents" id="editor_values_contents"></table>\n\
            <div><button type = "button" id="selectedNodeUpdateButton">Save</button></div> \n\
            </form>\n\
            </fieldset>\n\
          \n            ');
    }

    /**
     * Helper function. Finds nodes in the question XML structure.
     *
     * @param {array} pathAsArray The path, e.g. ['question', 'mapping'] to return all mappings
     * @param {object} config {"contentsTagNameMatch": string, "titleSrc": string, "valueSrc":
     *                           string} where contentsTagNameMatch is optional. If provided,
     *                           it looks in the node's contents to find a certain tag; titleSrc
     *                           and valueSrc are attribute names of the found node; these are
     *                           used for the return array. titleSrc can have the special value
     *                           @innerHTML to obtain the html inside the matched nodes
     * @returns {Array} Array of objects: {"title": string, "value": string}
     */
    function findNodes(pathAsArray, config) {
      var pathAsString = '.xmlJsonEditor_' + pathAsArray.join(' .xmlJsonEditor_') + '';
      var $elements = $xmlJsonEditor_tree_container.find(pathAsString);
      var returnObj = [], matchObj;
      var regExp;
      var match;
      /* helper function to get match value */
      var getMatchValue = function ($match, src) {
        /**
         * @param object $match A jQueryfied DOM/XML node
         * @param string src Attribute name or @innerHTML
         **/
        var returnValue = '';
        if (src === '@innerHTML') {
          /* get html */
          returnValue = $match.html();
        }
        else {
          /* get attribute value */
          if (jQuery.isArray(src)) {
            /* array of attributes provided */
            jQuery.each(src, function (i) {
              var attrValue = $match.attr(src[i]);
              returnValue += attrValue ? attrValue + ' ' : '';
            });
            /* remove spaces */
            returnValue = returnValue.trim();
          }
          else {
            /* single attribute provided */
            returnValue = $match.attr(src);
          }
        }
        return returnValue;
      };
      /**
       * Find matches in elements
       */
      $elements.each(function () {
        var $match;
        var $element = jQuery(this);
        var jstree_cqData = $element.data().jstree_cq;
        if (config.contentsTagNameMatch && jstree_cqData.content) {
          /* Grab name-value pairs from matched elements' contents */
          regExp = new RegExp('<' + config.contentsTagNameMatch + ' [^>]*>(([^<]*)(<\/' + config.contentsTagNameMatch + '>))?', 'ig');
          /* find tag inside contents */
          while ((match = regExp.exec(jstree_cqData.content)) !== null) {
            $match = jQuery(match[0]);
            matchObj = {
              "title": getMatchValue($match, config.titleSrc),
              "value": getMatchValue($match, config.valueSrc)
            };
            if (config.groupBySrc) {
              matchObj["group"] = getMatchValue($match, config.groupBySrc);
            }
            returnObj.push(matchObj);
          }
        }
        else if (jstree_cqData.attributes) {
          /* Grab name-value pairs directly from matched elements */
          matchObj = {
            "title": config.titleSrc === '@innerHTML' ? jstree_cqData.content : jstree_cqData.attributes[config.titleSrc],
            "value": jstree_cqData.attributes[config.valueSrc]
          };
          if (config.groupBySrc) {
            matchObj["group"] = jstree_cqData.attributes[config.groupBySrc];
          }
          returnObj.push(matchObj);
        }
      });
      return returnObj;
    }

    /**
     * Create an editor for the selected tree item.
     *
     * @param element
     *   The LI tree element to create the editor for.
     */
    function createEditorFor(element) {
      var listenerData;
      var editor, childElement;
      var data = element.data().jstree_cq;
      var itemConfig = getConfig(element);
      var editorElements = {};
      var title = itemConfig.title || data.type;
      emptyEditor();
      var $editorWrapper = jQuery('#xmlJsonEditor_editor');
      $editorWrapper.attr('data-element', title.toLowerCase());
      editor = jQuery("#editor_values_contents");
      editor.attr('data-element', title.toLowerCase());
      var iconHTML = '<img class="xmlJsonEditor_icon" src="' + getElementIconURL(element) + '" />';

      var addButtonWrapper = jQuery('#selectedNodeAddlist').empty();

      jQuery('#selectedNodeAddContainerTitle').text(title);
      jQuery("#selectedNodeUpdateButton").text("Save " + title);
      jQuery("#editor_values_legend label").html("Edit " + title);

      editorElements.forElement = getEditorElements(element);
      /* add editor for child elements? */
      if (itemConfig.children_in_editor) {
        editorElements.forChildren = [];
        jQuery(itemConfig.children_in_editor).each(function () {
          childElement = element.children("ul").children("[rel='" + this + "']");
          editorElements.forChildren.push(getEditorElements(childElement));
        });
      }

      if (!editorElements.forElement || (editorElements.forElement.attributeEditorElements.length === 0 && !editorElements.forChildren)) {
        $editorWrapper.append('<div class="xmlJsonEditor_form_description">' + Drupal.t('This item cannot be edited. Select another item or add a child item from the top bar under "Click to insert"') + '</div>');
        jQuery("#editor_values_legend label").html(iconHTML + ' ' + title);
      }
      else {
        jQuery("#editor_values_legend label").html(iconHTML + " Edit " + title);
      }

      /* create editor */
      if (editorElements.forElement.attributeEditorElements.length > 0 || editorElements.forChildren !== undefined) {
        jQuery("#editor_values").show();
        //for the element
        _appendEditorElements(editor, editorElements.forElement);
        //for its children
        jQuery(editorElements.forChildren).each(function () {
          _appendEditorElements(editor, this);
        });
      }
      else {
        jQuery("#editor_values").hide();
      }

      /* hide delete/clone buttons for question */
      if (title === 'question' || itemConfig.isDeletable === false) {
        jQuery('#selectedNodeRemoveContainer').hide();
      }
      else {
        jQuery('#selectedNodeRemoveContainer').show();
      }

      //add structure buttons for element
      var addStructureButtonsHandler = function (addButtonWrapper, element) {
        return function () {
          var itemConfig = getConfig(element);
          var addNodeOptions = getAddNodeOptions(itemConfig.valid_children, element);
          if (addNodeOptions && addNodeOptions.length > 0) {
            addButtonWrapper.empty();
            jQuery(addNodeOptions).each(function (i, $el) {
              if ($el !== undefined) {
                addButtonWrapper.append(this);
              }
            });
          }
        };
      }(addButtonWrapper, element);
      addStructureButtonsHandler();

      /* call listeners */
      listenerData = {
        "config": getConfig(),
        "editor": jQuery(editorSelector),
        "editorElements": editorElements,
        "treeNode": element,
        "type": data.type
      };
      if (notifyListeners('onLoadEditor', listenerData) === false) {
        /* hide editor */
        jQuery('#editor_values_contents').addClass('editor_hidden');
      }
      else {
        /* show editor */
        jQuery('#editor_values_contents').removeClass('editor_hidden');
      }

      jQuery("#editor_values :input, #editor_values textarea").data('addStructureButtonsHandler', addStructureButtonsHandler);

      /* enable auto-save */
      jQuery("#editor_values :input, #editor_values textarea").on('change keyup', function () {
        jQuery('#selectedNodeUpdateButton').data('isEdited', true);
        jQuery('#selectedNodeUpdateButton').trigger('click');
        jQuery(this).data('addStructureButtonsHandler')();
        return true;
      });
      // If save is pushed, we need to update the node.
      saveHandlers.push(
        function (element) {
          return function () {
            /* update title */
            var title = createTitleForElement(element);
            tree.jstree_cq("core").set_text(element, title);
            setElementTreeIcon(element, false, true);
//            dsfsdf
          };
        }(element));
      var attachTo = jQuery('#xmlJsonEditor_editor').get(0);
      Drupal.attachBehaviors(attachTo);
    }

    /**
     * Adds editor elements to the editor
     * @param editor
     *   A DOM node containing the editor
     * @param editorElements
     *   An object containing the editor elements, @see getEditorElements
     */
    function _appendEditorElements(editor, editorElements) {
      /* add elements to the editor/addButtonWrapper */
      if (editorElements.attributeEditorElements) {
        editorElements.attributeEditorElements.each(function () {
          editor.append(this);
        });
      }

      if (editorElements.description) {
        jQuery(editorElements.description).insertBefore(jQuery('#editor_values'));
      }
    }

    /**
     * Returns editor elements for the selected tree item.
     *
     * @param element
     *   The LI tree element to create the editor for.
     *
     * @return object
     *   An object containing the editor for the attributes, a description, and
     *   some buttons to alter the structure (e.g. add child, remove item) with
     *   the fields:
     *   - attributeEditor: DOM-node
     *   - description: DOM-node
     *   - structureButtons: object
     *     - addNodeOptions: array of DOM-node
     */
    function getEditorElements(element) {
      var attributeEditor;
      var description;
      var attName;
      var data;
      var structureButtons;
      var itemConfig;
      var i;
      data = element.data().jstree_cq;
      itemConfig = getConfig(element);

      if (itemConfig !== undefined) {
        // See if we should sort the attributes
        var attKeys = Object.keys(itemConfig.attributes);
        attKeys.sort(function (att_a, att_b) {
          if (typeof itemConfig.attributes[att_a].weight === 'undefined' && typeof itemConfig.attributes[att_b].weight !== 'undefined') {
            return 1;
          }

          if (typeof itemConfig.attributes[att_a].weight !== 'undefined' && typeof itemConfig.attributes[att_b].weight === 'undefined') {
            return -1;
          }

          if (typeof itemConfig.attributes[att_a].weight === 'undefined' && typeof itemConfig.attributes[att_b].weight === 'undefined') {
            return 0;
          }

          if (itemConfig.attributes[att_a].weight > itemConfig.attributes[att_b].weight) {
            return 1;
          }

          return 0;
        });

        attributeEditor = jQuery("<div></div>");
        for (i = 0; i < attKeys.length; i++) {
          attName = attKeys[i];
          /* the attribute can be nullified by getConfig */
          if (itemConfig.attributes[attName] !== null) {
            createAttributeEditor(element, itemConfig, data, attName, attributeEditor);
          }
        }

        if (itemConfig.content === 1) {
          createContentEditor(element, data, itemConfig, attributeEditor);
        }
        else if (itemConfig.content === 2) {
          createContentEditor(element, data, itemConfig, attributeEditor, false);
        }
        /* add buttons which allow user to add nodes */
        if (itemConfig.valid_children !== undefined) {
          structureButtons = {};
          structureButtons.addNodeOptions = getAddNodeOptions(itemConfig.valid_children, element);
        }

        if (itemConfig.description) {
          description = jQuery('<div class="xmlJsonEditor_form_description">' + itemConfig.description + '</div>');
        }
      }

      return {
        "attributeEditorElements": attributeEditor.children(),
        "description": description,
        "structureButtons": structureButtons
      };
    }

    /**
     * Creates a list of options for the add-node dropdown.
     *
     * @param valid_children
     *   The array of valid children to put in the dropdown.
     * @param contextElement
     *   The LI tree element to create the list for.
     */
    function getAddNodeOptions(valid_children, contextElement) {
      if (!valid_children) {
        return;
      }

      /* sort children */
      valid_children = valid_children.sort(function (child_a, child_b) {
        var childTypeConfigA = getConfig(child_a, contextElement);
        var childTypeConfigB = getConfig(child_b, contextElement);
        var title_a = childTypeConfigA.title !== undefined ? childTypeConfigA.title : child_a;
        var title_b = childTypeConfigB.title !== undefined ? childTypeConfigB.title : child_b;
        title_a = title_a.toLowerCase();
        title_b = title_b.toLowerCase();
        return title_a > title_b ? 1 : -1;
      });
      var numberOfValidChildren = valid_children.length;
      var addChildButton;
      var addNodeOptions = [];
      var childCount;
      var childType;
      var childTypeConfig;
      var childList;
      var maxCount;
      var title;
      var imageHTML;
      var config = getConfig();
      if (numberOfValidChildren > 0) {
        for (var i = 0; i < numberOfValidChildren; i++) {
          childType = valid_children[i];
          childTypeConfig = getConfig(childType, contextElement);
          childCount = 0;
          maxCount = 1;
          if (childTypeConfig.max_count && childTypeConfig.max_count >= 0) {
            maxCount = childTypeConfig.max_count;
            childList = contextElement.find('> ul > li[rel=' + childType + ']');
            childCount = childList.length;
          }

          if (childCount < maxCount) {
            if (!childTypeConfig.hidden || (childTypeConfig.hidden !== true || childTypeConfig.hidden.toString().toLowerCase() !== 'true')) {
              title = _ucfirst(childTypeConfig.title) || _ucfirst(childType);
              imageHTML = (childTypeConfig.icon && childTypeConfig.icon.image) ? '<img src="' + childTypeConfig.icon.image + '" /> ' : '';
              addChildButton = jQuery('<li data-value="' + childType + '"><span class="title">' + imageHTML + title + '</span><span class="description">' + (childTypeConfig.description || "") + '</span></li>');
              addChildButton.data('xmlEditornode.description', childTypeConfig.description); //store node description
              addChildButton.data('xmlEditornode.type', childType);
              addChildButton.data('xmlEditornode.contextElement', contextElement);

              addChildButton.on('click', function () {
                var $this = jQuery(this);
                $this.siblings().removeClass('selected');
                $this.addClass('selected');

                var childType = $this.data('xmlEditornode.type');
                var contextElement = $this.data('xmlEditornode.contextElement');

                tree.xmlTreeEditor('addNode', childType, contextElement);
              });
              addNodeOptions[i] = addChildButton;
            }
          }
        }
      }
      return addNodeOptions;
    }

    /**
     * Helper function. Fetches child tags from tree.
     *
     * @param {object} attConfigAutoValues
     * @returns {Array}
     *   Array of objects {"title": string, "value": string}
     */
    function _getAttConfigAutoValuesChildTags(attConfigAutoValues) {
      var childTags, returnTags = [];
      var i;

      if (!jQuery.isArray(attConfigAutoValues.path[0])) {
        attConfigAutoValues.path = [attConfigAutoValues.path];
      }

      for (i = 0; i < attConfigAutoValues.path.length; i++) {
        childTags = findNodes(attConfigAutoValues.path[i], {
          "contentsTagNameMatch": attConfigAutoValues.contentsTagNameMatch,
          "valueSrc": attConfigAutoValues.valueSrc,
          "titleSrc": attConfigAutoValues.titleSrc,
          "groupBySrc": attConfigAutoValues.groupBySrc
        });

        returnTags = jQuery.merge(returnTags, childTags);
      }

      returnTags.sort(function (el1, el2) {
        var t1 = el1.title ? el1.title.toUpperCase() : '';
        var t2 = el2.title ? el2.title.toUpperCase() : '';
        var g1 = el1.group ? el1.group.toUpperCase() : '';
        var g2 = el2.group ? el2.group.toUpperCase() : '';

        if (g1 === g2) {
          return t1 < t2 ? -1 : 1;
        }
        else {
          return g1 < g2 ? -1 : 1;
        }
      });

      return returnTags;
    }

    /**
     * Creates an editor element for an attribute and adds it to the given
     * editor block.
     *
     * @param element
     *   The LI tree element of the item that the attibute belongs to.
     * @param itemConfig
     *   The config data of the item that the attibute belongs to.
     * @param itemData
     *   The data of the item that the attibute belongs to.
     * @param attName
     *   The name of the attribute to create the editor for.
     * @param editor
     *   The editor HTML dom element to add the content editor to.
     */
    function createAttributeEditor(element, itemConfig, itemData, attName, editor) {
      var $attributeFormElement, $optionElement, $attributeWrapperElement, $attributeGuiElement, $attributeLabelElement, $attributeDescriptionElement;
      var attConfig = itemConfig.attributes[attName];
      var numberOfValues, disabledAttr;
      var attValue = "";
      var config = getConfig();
      if (itemData.attributes[attName] !== undefined) {
        attValue = itemData.attributes[attName];
      }
      var attId = attName;
      var item;
      var childTags;
      var itemValue;
      var itemTitle;
      var title;
      var $currentOptGroup, currentGroup = '';
      var noValueFunction;
      var default_hint_on_empty = {
        'string': 'Enter text',
        'int': 'Enter a number'
      };
      if (typeof (attConfig) === "string") {
        /* no config provided, create minimalistic config */
        attConfig = {
          "type": attConfig,
          "hint_on_empty": default_hint_on_empty[attConfig]
        };
      }

      /* create wrapper element */
      $attributeWrapperElement = jQuery('<tr class="xmlJsonEditor_attribute xmlJsonEditor_attribute_container" data-attribute="' + attId.toLowerCase() + '"></tr>');

      /* create editor */
      if (attConfig.alias_of === undefined && !(attConfig.deprecated === 1 && attValue.length === 0)) {
        /* Check if the attribute is deprecated. If it is, and if it is empty
         * then don't show it. */
        if (attConfig.deprecated === 1) {
          $attributeWrapperElement.addClass("deprecated");
        }

        /* set title */
        if (attConfig.title !== undefined) {
          title = attConfig.title;
        }
        else {
          title = attName;
        }

        /* hide this editor? */
        if (attConfig.hidden && (attConfig.hidden !== true || attConfig.hidden.toString().toLowerCase() !== 'true')) {
          $attributeWrapperElement.css("display", "none");
        }

        /* create label element */
        $attributeLabelElement = jQuery('<th class="xmlJsonEditor_attribute_title">' + title + ':</th>');
        /* get list with options if auto_values is set */
        if (attConfig.auto_values !== undefined) {
          childTags = _getAttConfigAutoValuesChildTags(attConfig.auto_values);

          if (childTags.length > 0) {
            attConfig.values = jQuery.merge([{"title": "- select -", "value": ""}], childTags);
          }
          else {
            return; //Skip this form element, as it has no valid values
          }

          /* check if current att value is in options array, if not: destroy options array */
          var numberOfValues = attConfig.values.length;
          var valuesArr = [];
          for (i = 0; i < numberOfValues; i++) {
            itemValue = '-empty-';
            item = attConfig.values[i];
            if (typeof item === "object" && typeof item.value === 'string') {
              itemValue = item.value.toString().toLowerCase();
            }
            else if (typeof item === 'string' || typeof item === 'number') {
              itemValue = item.toString().toLowerCase();
            }
            valuesArr.push(itemValue);
          }

          if (valuesArr.indexOf(attValue.toLowerCase()) === -1) {
            attConfig.values.push({
              "title": attValue, "value": attValue
            });
          }
        }

        /* create form element */
        if (jQuery.isArray(attConfig.values)) {
          numberOfValues = attConfig.values.length;
          disabledAttr = numberOfValues === 1 ? ' disabled' : '';
          /* select with pre-set values */
          $attributeFormElement = jQuery('<select id="xmlEditor_' + attId + '" name="' + attId + '" size="1"' + disabledAttr + '></select>');
          for (var i = 0; i < numberOfValues; i++) {
            itemValue = null;
            itemTitle = null;

            item = attConfig.values[i];
            if (typeof item === "object" && (typeof item.value === 'string' || typeof item.value === 'number')) {
              itemValue = item.value.toString();
              itemTitle = item.title;

              if (item.group !== currentGroup) {
                if (item.group) {
                  // Make sure options will be added inside optgroup
                  $currentOptGroup = jQuery('<optgroup label="Group ' + item.group + '" />');
                  $attributeFormElement.append($currentOptGroup);
                  currentGroup = item.group;
                }
                else {
                  // Make sure options will be added to select directly
                  $currentOptGroup = null;
                }
              }
            }
            else if (typeof item === 'string' || typeof item === 'number') {
              itemValue = item.toString();
              itemTitle = itemValue;
            }

            if (itemValue !== null && itemTitle !== null) {
              $optionElement = jQuery('<option value="' + itemValue + '">' + _ucfirst(itemTitle) + '</option>');
              //should this option be selected?
              if (itemValue.toLowerCase() === attValue.toLowerCase()) {
                $optionElement.attr("selected", "selected");
              }

              // Add option
              if ($currentOptGroup) {
                $currentOptGroup.append($optionElement);
              }
              else {
                $attributeFormElement.append($optionElement);
              }
            }
          }

          if (childTags && childTags.length === 0) {
            $attributeFormElement.attr('disabled', 'disabled');
          }
        }
        else {
          /* text element */
          var width = attConfig.width ? ' style="width: ' + attConfig.width + '" ' : '';
          $attributeLabelElement = jQuery('<th class="xmlJsonEditor_attribute_title">' + _ucfirst(title) + ':</th>');

          $attributeFormElement = jQuery('<input id="xmlEditor_' + attId + '" ' + width + ' type="text" />');
          $attributeFormElement.val(attValue);
        }

        /* set default value */
        if (attConfig.default_value && !attValue) {
          $attributeFormElement.val(attConfig.default_value);
        }


        /* append all elements to wrapper */
        $attributeWrapperElement.append($attributeLabelElement);
        $attributeLabelElement.append($attributeDescriptionElement);
        var wrap = jQuery('<td class="xmlJsonEditor_attribute_container_wrap"></td>');
        $attributeWrapperElement.append(wrap);
        wrap.append($attributeFormElement);

        /* attributes can be inside group */
        if (attConfig.attributeOptionGroup) {
          $attributeGuiElement = jQuery('<div class="xmlJsonEditor_attribute_gui"></div>');
          var $option = jQuery('<input type="radio" name="' + attConfig.attributeOptionGroup + '" value="' + attName + '"/>');
          if (attValue !== '' && attValue !== undefined) {
            $option.attr('checked', 'checked');
          }
          $attributeWrapperElement.attr('data-attribute-group', attConfig.attributeOptionGroup);
          $attributeWrapperElement.attr('data-attribute-group-value', attName);

          $option.on('click', function (e) {
            var $this = jQuery(this);

            var $attributeFormElementsInGroup = jQuery('[data-attribute-group=' + attConfig.attributeOptionGroup + ']').not('[data-attribute-group-value=' + $this.val() + ']');

            $attributeFormElementsInGroup.find(':input, textarea').val('').trigger('change');
          });
          $attributeGuiElement.append($option);
          $attributeWrapperElement.addClass('xmlJsonEditor_attribute_gui');
          $attributeFormElement.on('focus', function () {
            $option.trigger('click');
          });
          wrap.prepend($attributeGuiElement);
        }


        /* append wrapper element to editor */
        if ($attributeWrapperElement.html() !== '') {
          /* append feedback element to wrapper */
          wrap.append('<div class="xmlEditorAttributeFeedback"></div>');
          editor.append($attributeWrapperElement);
        }

        /* set feedback? */
        if (attConfig.feedback) {
          if ($attributeFormElement[0].nodeName.toString().toLowerCase() === 'select') {
            /* give feedback onchange */
            $attributeFormElement.change(function () {
              //but only if the value differs from hint_on_empty
              if (!attConfig.hint_on_empty || (jQuery(this).val() === attConfig.hint_on_empty && jQuery(this).data('xmlEditor.attribute_value_set_by_user') === true)) {
                handleAttributeFeedback(jQuery(this), attConfig.feedback);
              }
            });
            //trigger event to show feedback
            $attributeFormElement.change();
          }
          else {
            /* give feedback onkeyup */
            $attributeFormElement.keyup(function () {
              //but only if the value differs from hint_on_empty
              if (!attConfig.hint_on_empty || (jQuery(this).val() === attConfig.hint_on_empty && jQuery(this).data('xmlEditor.attribute_value_set_by_user') === true)) {
                handleAttributeFeedback(jQuery(this), attConfig.feedback);
              }
            });
            //trigger event to show feedback
            $attributeFormElement.keyup();
          }
        }

        /* add description */
        if (attConfig.description !== undefined) {
          $attributeDescriptionElement = jQuery('<img class="xmlJsonEditor_help_icon" src="' + config.basePath + '/assets/icons/question_icon.png" title="' + attConfig.description + '" />');
          updateFormElementFeedback($attributeFormElement, 'description', {
            "type": "description",
            "text": attConfig.description
          });
        }

        /* add extras for specific attribute types */
        if (attConfig.type === 'regexp') {
          /* add support buttons
           */
          var enabledPresets = jQuery.isArray(attConfig.enabledPresetPatternIds) ? attConfig.enabledPresetPatternIds : [];
          /* add preset select */

          //NOTE: never change below IDs, as these are used in the xml config! */
          var presets = {
            "general": [
              {"id": "gen_empty", "title": "Empty answer", "pattern": "^$"},
              {"id": "gen_all", "title": "Any answer", "pattern": ".+"}
            ],
            "hotspots": [

            ],
            "selectorder": [
              {"id": "so_abd", "title": "Options with ids a, b and d", "pattern": "^abd$", "description": "^ means: beginning of answer; $ means: end of answer"},
              {"id": "so_b_before_c", "title": "Option b should always precede option c", "pattern": "[^b]*c", "description": "[^b] means: NOT b; * means: 0...n times; so [^b]*c means 'match anything that is not 'b' until you find a 'c' "},
              {"id": "so_multiple_1", "title": "(Multiple target boxes) Options a, b and c in target 1, option d in target 2", "pattern": "^1abc2d$", "description": "^ means: beginning of answer; $ means: end of answer"}
            ],
            "numbers": [
              {"id": "num_any", "title": "Any number", "pattern": "^[+-]?[0-9]+([,.][0-9]+)?([eE][+-]?[0-9]+)?$"},
              {"id": "num_no_digit", "title": "Number without decimals", "pattern": "^[-+]?[0-9]+$"},
              {"id": "num_one_digit", "title": "Number with one decimal", "pattern": "^[-+]?[0-9]*\\.[0-9]{1}$"},
              {"id": "num_two_digit", "title": "Number with two decimals", "pattern": "^[-+]?[0-9]*\\.?[0-9]{2}$"},
              {"id": "num_three_digit", "title": "Number with three decimals", "pattern": "^[-+]?[0-9]*\\.?[0-9]{3}$"}
            ],
            "interpunction": [
              {"id": "int_comma", "title": "Comma as decimal separator", "pattern": "^[-+]?[0-9]*\,[0-9]*$"}
            ],
            "options": [
              {"id": "txt_only_a", "title": "Only answer 'a'", "pattern": "^a$"},
              {"id": "txt_only_ac", "title": "Only answer 'a' and 'c'", "pattern": "^ac$"},
              {"id": "txt_contains_a", "title": "Contains 'a'", "pattern": "a"}
            ]};

          // Get ourselves a select with presets.
          var $presetsSelect = jQuery('<select id="xmlEditor_presets_select"></select>');
          $presetsSelect.append('<option value="">-Custom search term-</option>');

          jQuery.each(presets, function (presetType, presets) {
            /* add presets per type to select */
            var $optGroup;
            var optionHTML;

            // is preset group disabled?
            optionHTML = '';
            jQuery.each(presets, function (i, preset) {
              /* only show enabled presets */
              if (enabledPresets.indexOf(preset.id) !== -1 || enabledPresets.indexOf(presetType) !== -1) { // is preset disabled?
                optionHTML += '<option value="' + preset.pattern + '">' + preset.title + '</option>';
              }
            });

            if (optionHTML !== '') {
              $optGroup = jQuery('<optgroup label="' + (presetType.charAt(0).toUpperCase() + presetType.slice(1)) + '"></optgroup>').append(optionHTML);
              $presetsSelect.append($optGroup);
            }
          });

          $presetsSelect.on('change', function () {
            var presetVal = jQuery(this).val();
            var attributeVal = $attributeFormElement.val();
            if (presetVal !== attributeVal) { //prevent loop of two elements trigger each other change event
              $attributeFormElement.val(jQuery(this).val()).trigger('change');
            }
          });

          $attributeFormElement.on('keyup change', function () {
            var presetVal = $presetsSelect.val();
            var attributeVal = jQuery(this).val();

            var exists = false;
            $presetsSelect.find('option').each(function () {
              if (this.value === attributeVal) {
                exists = true;
                return false;
              }
            });

            if (exists) {
              if (presetVal !== attributeVal) { //prevent loop of two elements trigger each other change event
                $presetsSelect.val(attributeVal).trigger('change');
              }
            }
            else {
              $presetsSelect.val(""); //Custom search term
            }
          });
          $attributeFormElement.trigger('change');

          $attributeFormElement.before($presetsSelect);


          /* tester */
          var $testButton = jQuery('<a style="margin-left: 16px;" href="javascript:void(0)"><span style="display:inline">Show tester</span><span style="display:none">Hide tester</span></a>');
          var $testCoach = jQuery('<div class="xmleditor_regexpcoach" style=" padding:16px;">Test answer: <input type="text" placeholder="Enter answer" style="width: 50%" /> <span></span> with pattern</div>');
          $attributeFormElement.data('xmleditor.regexpcoach', $testCoach);
          $testCoach.data('xmleditor.regexpcoach', $testCoach);

          var keyupHandler = function () {
            var $testCoach = $attributeFormElement.data('xmleditor.regexpcoach');
            var $testCoachInput = $testCoach.children('input');
            var $testCoachFeedback = $testCoach.children('span');

            try {
              var re = new RegExp($attributeFormElement.val(), 'g');

              var testVar = $testCoachInput.val().toString();

              if (jQuery.isArray(re.exec(testVar)) === true) {
                $testCoachFeedback.html('matches');
              }
              else {
                $testCoachFeedback.html('<b>does not match</b>');
              }
              $attributeFormElement.css('color', '');
            } catch (e) {
              $testCoachFeedback.html('<b style="color:red">(error in pattern)</b>');
              $attributeFormElement.css('color', 'red');
            }

          };
          $attributeFormElement.on('keyup', keyupHandler);
          $testCoach.children('input').on('keyup', keyupHandler);

          $testButton.on('click', function () {
            jQuery(this).children('span').toggle();
            $testCoach.toggle(500);
          });


          /* wildcards */
          var $wildcardButton = jQuery('<a style="margin-left: 16px;" href="javascript:void(0)"><span style="display:inline">Show wildcards</span><span style="display:none">Hide wildcards</span></a>');
          var $wildcardsTable = jQuery('\
            <div style="display:none; padding:16px;">\n\
              <table border="1" cellspacing="0" cellpadding="0">\n\
                <tbody>\n\
                  <tr><th>Wildcard</th><th>Meaning</th><th>Example</th></tr>\n\
                  <tr><td>^</td><td>Beginning of answer</td><td><strong>^bc</strong> will match with <strong>bcd</strong> but not with <strong>abc</strong>, because <strong>abc</strong> does not start with <strong>bc</strong></td></tr>\n\
                  <tr><td>$</td><td>End of answer</td><td><strong>bc</strong><strong>$</strong> will match with <strong>abc</strong> but not with <strong>abcd</strong>, because <strong>abcd</strong> does not end with <strong>bc</strong></td></tr>\n\
                  <tr><td>.</td><td>Any character</td><td><strong>a.c</strong> will match with both <strong>abc</strong> and <strong>adc</strong></td></tr><tr><td>[ and ]</td><td>A range of characters</td><td><strong>[a-e] </strong>will match <strong>a</strong>,<strong> b</strong>,<strong> c</strong>,<strong> d </strong>and<strong> e</strong></td></tr>\n\
                  <tr><td>+</td><td>Repeat previous 1 or more times.</td><td><strong>[0-9]+ </strong>will match any number. <strong>a+b</strong> will match <strong>ab</strong>, <strong>aab</strong> and <strong>aaab</strong></td></tr>\n\
                  <tr><td>*</td><td>Repeat previous 0 or more times.</td><td><strong>1*23</strong> will match <strong>123</strong>, <strong>11111123</strong> and <strong>23</strong></td></tr>\n\
                </tbody>\n\
              </table>\n\
              <p>When matching answers which contain wildcards, e.g. the answer <strong>$500</strong>, a <strong>\\</strong> should be added before the wildcard. For example: the correct search term to match <strong>$500 </strong>is <strong>\\$500</strong> (and not <strong>$500</strong>)</p>\n\
            </div>');

          $wildcardButton.on('click', function () {
            jQuery(this).children('span').toggle();
            $wildcardsTable.toggle(500);
          });

          /**
           * Add elements to DOM
           */
          $attributeWrapperElement.find('.xmlEditorAttributeFeedback').append($testButton, $wildcardButton);
          $attributeWrapperElement.find('.xmlEditorAttributeFeedback').append($testCoach, $wildcardsTable);

          keyupHandler(); // Call handler of reg exp helper
        }

        if (attConfig.type === 'file') {
          if (CQ_MediaModule2IsEnabled()) {
            var $media_preview = jQuery('<div></div>');
            if (attValue.length > 0 && attValue.indexOf('[attachurl') === -1) {
              $media_preview.append(CQ_ImgHtmlFromMediatag(attValue));
            }
            else {
              $media_preview.append('<img alt="' + attValue + '" />');
            }

            $media_preview.find('img').css({"max-width": 200, "max-height": 200});
            if (attValue === undefined || attValue === '') {
              $media_preview.hide();
            }

            var $media_popup_button = jQuery('<button style="width:15%;" type="button">Browse</button>');
            $attributeFormElement.css('width', '80%');
            $attributeFormElement.after($media_popup_button);
            $attributeFormElement.before($media_preview);
            $attributeFormElement.hide();

            // This function will be called by Media Module media Browser after user selected media.
            var callBack = function (options) {
              var mediaInfo = options[0];
              $attributeFormElement.val('[[{"fid":' + mediaInfo.fid + ',"type":"media","view_mode":"default"}]]');
              $media_preview.find('img').attr('src', drupalSettings.basePath + 'closedquestion/getmedia/' + mediaInfo.fid);
              $media_preview.show();
              $attributeFormElement.trigger('change');
            };

            $media_popup_button.on('click', function () {
              Drupal.media.popups.mediaBrowser(callBack, {"file_extensions": 'png gif jpg jpeg'});
            });
          }
        }

        /* add styling/css */
        if (attConfig.css) {
          if (jQuery.isArray(attConfig.css)) {
            /* array with class names */
            jQuery(attConfig.css).each(function () {
              $attributeFormElement.addClass(this.toString());
            });
          }
          else {
            /* css name/value object */
            $attributeFormElement.css(attConfig.css);
          }
        }

        /* automatically add a hint when the element has no value
         * @todo: make this work for non-textual form elements, like select,
         * checkboxes, etc.
         */
        if (attConfig.hint_on_empty) {
          /* add hint when no value is set right now */
          if (!attValue) {
            $attributeFormElement.val(attConfig.hint_on_empty);
            $attributeFormElement.data('xmlEditor.attribute_value_set_by_user', false);
            $attributeFormElement.addClass('xmlEditor_empty');
          }

          /* remove hint when element gets focus */
          $attributeFormElement.focus(function () {
            if (jQuery(this).val() === attConfig.hint_on_empty && $attributeFormElement.data('xmlEditor.attribute_value_set_by_user') === false) {
              jQuery(this).val('');
              $attributeFormElement.removeClass('xmlEditor_empty');
            }
          });
          /* remember when user does something */
          $attributeFormElement.keyup(function () {
            $attributeFormElement.data('xmlEditor.attribute_value_set_by_user', true);
          });
          /* add hint when element gets no value */
          noValueFunction = function () {
            if (jQuery(this).val() === "") {
              jQuery(this).val(attConfig.hint_on_empty);
              $attributeFormElement.data('xmlEditor.attribute_value_set_by_user', false);
              $attributeFormElement.addClass('xmlEditor_empty');
            }
          };
          $attributeFormElement.blur(noValueFunction);
          $attributeFormElement.change(noValueFunction);
        }

        /* element has an editor, attach a change listener to it. */
        var attEditor = jQuery(".xmlJsonEditor_attribute #xmlEditor_" + attId + "", editor);
        saveHandlers.push(
          function (element, name, attEditor, attConfig) {
            return function () {

              var data = element.data();
              var type = (data.jstree_cq && data.jstree_cq.type) ? data.jstree_cq.type : undefined;
              var newValue = attEditor.val();
              if (newValue === attConfig.hint_on_empty && attEditor.data('xmlEditor.attribute_value_set_by_user') !== true) {
                //remove the hint_on_empty value before node is saved
                newValue = "";
              }
              if (newValue.length > 0 && newValue !== " ") { // Remove attribute when it is a space. @todo
                data.jstree_cq.attributes[name] = newValue;
              }
              else {
                delete element.data().jstree_cq.attributes[name];
              }
              notifyListeners('change', {
                "element": element,
                "type": type,
                "what": "attribute",
                "which": name,
                "value": newValue
              });
            };
          }(element, attName, attEditor, attConfig));
      }
    }

    /**
     * Creates an editor element for "content" and adds it to the given editor
     * block.
     *
     * @param element
     *   The LI tree element of the item that the attibute belongs to.
     * @param data
     *   The data of the item to create the editor for.
     * @param itemConfig
     *   The config of the editor itself.
     * @param editor
     *   The editor HTML dom element to add the content editor to.
     * @param isHTML
     *   Default: true. If true, a wysiwyg editor is created, otherwise a simple input.
     */
    function createContentEditor(element, data, itemConfig, editor, isHTML) {
      isHTML = typeof isHTML === 'undefined' ? true : isHTML;
      var config = getConfig();

      if (isHTML === true) {
        var $wrapper = jQuery("<td colspan='2' class='xmlJsonEditor_attribute form-textarea-wrapper resizable'><label>Content:</label></td>");
        var $wrapper_wrapper = jQuery('<tr class="xmlJsonEditor_attribute xmlJsonEditor_attribute_container"></tr>');
        editor.append($wrapper_wrapper);
        $wrapper_wrapper.append($wrapper);
        var $editorWrapper = jQuery('<div class="xmlJsonEditor_editor" />');
        $wrapper.append($editorWrapper);

        var $buttonWrapper = jQuery('<div class="xmlJsonEditor_buttons" />');

        $editorWrapper.append($buttonWrapper);

        var $textarea = jQuery("<textarea rows='20' name='cq_editor_content' id='cq_editor_content' style='display:none;font-family:Courier;font-size:10pt;background:black;color:white;' class='form-textarea img_assist resizable'></textarea>");

        /* Add code button */
        var $toggleCodeButton = jQuery('<button type="button" style="width:60px;border-radius:0;float:right" class="button">Code</button>');

        var $wysiwygEditor = jQuery('<div contentEditable="true" class="xmlJsonEditorContentWysiwyg"></div>');


        /* function to save the caret position */
        $wysiwygEditor.data('saveCaretPosition', function () {
          var sel = document.getSelection();
          $wysiwygEditor.data('savedCaretPosition', [sel.focusNode, sel.focusOffset]);
        });

        /* function to restore the caret position */
        $wysiwygEditor.data('restoreCaretPosition', function () {
          var sel = document.getSelection();
          var saved = $wysiwygEditor.data('savedCaretPosition');
          sel.collapse(saved[0], saved[1]);
          $wysiwygEditor[0].focus();
        });

        /**
         * Syncs the wysiwyg content editor with the underlying textarea holding
         *  the data.
         * @param {boolean} toWysiwyg
         *   If true, wysiwyg content editor gets the textarea's content. If false
         *    (default) the textarea gets the wysiwyg content editor's content.
         */
        function _syncWysiwygAndTextarea(toWysiwyg) {
          var i, matches, inlineTag, xmlFromString, imgHTML;
          var $images, $image, mediaTag, html = '', xmlSafeHTML;
          var special_element_definitions = $wysiwygEditor.data('xmleditor_element_definition');

          if (toWysiwyg === true) {
            /* Update the Wysiwyg editor. */
            html = $textarea.val();

            // Convert Media tags to <img> tags.
            if (CQ_MediaModule2IsEnabled() === true) {
              matches = html.match(/\[\[.*?\]\]/g);
              if (matches) {
                inlineTag = "";
                for (i = 0; i < matches.length; i++) {
                  inlineTag = matches[i];
                  imgHTML = CQ_ImgHtmlFromMediatag(inlineTag);
                  html = html.replace(inlineTag, imgHTML);
                }
              }
            }

            // Add zero-width spaces before special tags and before <br />, to ease caret selection in editor
            html = html.replace(/[\u200B]/g, '');
            jQuery.each(special_element_definitions, function (elementName, elementDefinition) {
              var tag = elementDefinition.tagName;
              var re = new RegExp('(<' + tag + '[^>]*>)', 'ig');
              html = html.replace(re, '\u200B$1\u200B');
            });

            $wysiwygEditor.html(html);

            /*
             * Do 3 things:
             * 1) Add innerHTML to special elements.
             * 2) Set unselectable attribute to elements (http://stackoverflow.com/questions/22318586/disable-img-resize-handles-in-ie-8-11-in-contenteditable-and-remove-them-if-po)
             * 3) Also unset contenteditable for these elements.
             */
            jQuery.each(special_element_definitions, function (i, element_def) {
              //1)
              var elementName = element_def.tagName;
              jQuery(elementName, $wysiwygEditor).each(function () {
                var $element = jQuery(this);
                var outerHTMLAttributes = getDomElementAttributes($element);

                if (_doSpecialElementCheck(elementName, outerHTMLAttributes, element_def) === false) {
                  return;
                }

                var innerHTML = _getSpecialElementInnerHTML(element_def.title, outerHTMLAttributes, element_def.innerHTMLAttributes);
                $element.html(innerHTML);
              });

              //2) and 3)
              jQuery(elementName, $wysiwygEditor).attr('unselectable', 'on');
              jQuery(elementName, $wysiwygEditor).attr('contenteditable', 'false');
            });
          }
          else {
            /* Update textarea */

            // Put HTML in temp container.
            var $tempContainer = jQuery('<div style="display:none"></div>');
            jQuery('body').append($tempContainer);
            $tempContainer.html($wysiwygEditor.html());

            // Convert image tags to Media tags.
            if (CQ_MediaModule2IsEnabled() === true) {
              $images = $tempContainer.find('img.media-element, img.media-image');
              jQuery.each($images, function (i, image) {
                $image = jQuery(image);
                mediaTag = CQ_MediatagFromImg($image);
                $tempContainer.find('img.media-element, img.media-image').eq(0).replaceWith(mediaTag);
              });
            }

            html = $tempContainer.html();

            // Remove zero-width spaces before tags, which ease caret selection in wysiwyg editor
            html = html.replace(/[\u200B]/g, '');

            // Get correct xhtml.
            xmlSafeHTML = htmlToXHTML(html, special_element_definitions);

            // Remove unselectable and contenteditable attributes
            xmlSafeHTML = xmlSafeHTML.replace(/unselectable="on"/ig, '');
            xmlSafeHTML = xmlSafeHTML.replace(/contenteditable="false"/ig, '');

            // Update textarea.
            $textarea.val(xmlSafeHTML);

            // Try to parse XML.
            xmlFromString = loadXMLFromString(xmlSafeHTML, true);

            if (xmlFromString.success === false) {
              updateFormElementFeedback($textarea, 'xmlParse', {
                "type": "error",
                "text": 'Warning: This text is not well-formed, which might lead to errors. <a href="javascript:void(0)" onclick="jQuery(this).next().toggle()">Technial details</a> <span style="display:none">' + xmlFromString.errorMessage + '</span> / <a href="javascript:void(0)" onclick="jQuery(this).next().toggle()">How to fix</a> <span style="display:none">1) Make sure all tags are closed, e.g. "&lt;p&gt;...&lt;/p&gt;" 2) Fix unsupported HTML entities, e.g. "&amp;amp;rightarr;"</span>'
              });
            }
            else {
              updateFormElementFeedback($textarea, 'xmlParse', undefined);
            }

            $tempContainer.remove();

            // Trigger save, but not an update of wysiwyg (which is triggered by keyup).
            $textarea.trigger('change');
          }

          _addWysiwygSpecialElementHandlers();
        }

        /**
         * Adds click handlers to special elements the user can insert, so that
         *  the user can edit them.
         */
        function _addWysiwygSpecialElementHandlers() {
          /* listen to resizing of images etc and sync with textarea */
          jQuery('img', $wysiwygEditor).each(function () {
            var $img = jQuery(this);
            var currentObserver = $img.data('MutationObserver');
            if (!currentObserver) {
              // create an observer instance for img, which will triggery sync with textarea.
              var observer = new MutationObserver(function () {
                window.clearTimeout($img.data('xmleditor_timeout'));
                $img.data('xmleditor_timeout', window.setTimeout(function () {
                  _syncWysiwygAndTextarea();
                }, 250));
              });
              observer.observe(this, {attributes: true});

              $img.data('MutationObserver', observer);
            }
          });

          var definitions = $wysiwygEditor.data('xmleditor_element_definition');

          /* Make it possible to edit special elements in editor */
          if (definitions) {
            jQuery.each(definitions, function (i, elementDefinition) {
              jQuery(elementDefinition.tagName, $wysiwygEditor).each(function () {
                var $element = jQuery(this);
                if (_doSpecialElementCheck(elementDefinition.tagName, getDomElementAttributes($element), elementDefinition) === false) {
                  return;
                }

                $element.unbind('click.xmleditor');
                $element.attr('unselectable', 'on'); //Prevent resize handlers http://stackoverflow.com/questions/22318586/disable-img-resize-handles-in-ie-8-11-in-contenteditable-and-remove-them-if-po

                /* When user clicks element a dialog should be visible with current values,
                 which should be editable.
                 */
                $element.bind('click.xmleditor', function () {
                  var $element = jQuery(this);
                  var currentAttValue;

                  /* create clone of attributes part of definition, so we can assign the current values to it */
                  var attributesDefinition = jQuery.extend(true, {}, elementDefinition.attributes);
                  jQuery.each(attributesDefinition, function (i, attDef) {
                    if (attDef.name !== '_innerHTML') {
                      currentAttValue = $element.attr(attDef.name);
                      if (currentAttValue !== undefined && currentAttValue !== null) {
                        attDef.value = currentAttValue;
                      }
                    }
                    else {
                      attDef.value = $element.html();
                    }
                  });

                  /* get a modal showing a form with current values */
                  var promptHtmlElements = _getPopupModalHTMLElements(attributesDefinition);

                  popupModal(promptHtmlElements, function callBack(response) {
                    // Check if user pressed delete
                    if (response === 'delete') {
                      $element.remove();
                      $wysiwygEditor.trigger('keyup'); // Trigger sync with textarea.
                      return;
                    }

                    // User pressed 'ok': set new attribute values to element.
                    var attributes = response;
                    jQuery.each(attributes, function (name, value) {
                      if (value === null) {
                        delete attributes[name]; // Remove empty attributes
                      }
                      if (name === "_innerHTML") {
                        $element.html(value);
                        delete attributes[name];
                      }
                    });

                    // Add special attributes for special elements
                    if (elementDefinition.innerHTMLAttributes) {
                      var innerHTML = _getSpecialElementInnerHTML(elementDefinition.title, attributes, elementDefinition.innerHTMLAttributes);
                      $element.html(innerHTML);
                    }

                    $element.attr(attributes);
                    $wysiwygEditor.trigger('keyup'); // Trigger sync with textarea.
                  }, true); // include delete button.
                });
              });
            });
          }
        }

        $toggleCodeButton.bind('click', function () {
          $textarea.toggle();
        });

        /**
         * Handle keyup event for textarea, which will sync the contents with the
         * wysiwyg.
         */
        $textarea.bind('keyup', function () {
          window.clearTimeout($textarea.data('xmleditor_timeout'));
          $textarea.data('xmleditor_timeout', window.setTimeout(function () {
            _syncWysiwygAndTextarea(true);
          }, 250));
        });

        /**
         * Handle keyup event for wysiwyg, which will sync the contents with the
         * textarea.
         */
        $wysiwygEditor.bind('keyup', function () {
          window.clearTimeout($wysiwygEditor.data('xmleditor_timeout'));
          $wysiwygEditor.data('xmleditor_timeout', window.setTimeout(function () {
            _syncWysiwygAndTextarea();
          }, 500));
        });

        /**
         * Handle paste event for wysiwyg, which strips out all HTML.
         */
        $wysiwygEditor.bind("paste", function (e) {
          var sel, range, text;
          e = e.originalEvent;
          e.preventDefault();

          /* W3C compliant browsers. */
          if (e.clipboardData && e.clipboardData.getData) {
            var text = e.clipboardData.getData("text/plain");
            document.execCommand("insertHTML", false, text);
          }
          /* IE */
          else if (window.clipboardData && window.clipboardData.getData) {
            var text = window.clipboardData.getData("Text");
            if (window.getSelection) {
              sel = window.getSelection();
              if (sel.getRangeAt && sel.rangeCount) {
                range = sel.getRangeAt(0);
                range.deleteContents();
                range.insertNode(document.createTextNode(text));
              }
            }
            else if (document.selection && document.selection.createRange) {
              document.selection.createRange().text = text;
            }
          }
        });

        /* add buttons */
        var buttons = {
          "i": {
            "title": '<img src="' + config.basePath + '/assets/icons/italic_icon.png" />',
            "description": "Italic",
            "tagName": "i",
            "command": "italic",
            "selfClosing": false
          }, "b": {
            "title": '<img src="' + config.basePath + '/assets/icons/bold_icon.png" />',
            "description": "Bold",
            "tagName": "b",
            "command": "bold",
            "selfClosing": false
          }, //
          "sub": {
            "title": '<img src="' + config.basePath + '/assets/icons/sub_icon.png" />',
            "description": "Subscript",
            "tagName": "sub",
            "command": "subscript",
            "selfClosing": false
          },
          "sup": {
            "title": '<img src="' + config.basePath + '/assets/icons/sup_icon.png" />',
            "description": "Superscript",
            "tagName": "sup",
            "command": "superscript",
            "selfClosing": false
          },
          "list": {
            "title": '<img src="' + config.basePath + '/assets/icons/list_icon.png" />',
            "description": "list",
            "tagName": "ul",
            "command": "insertUnorderedList",
            "selfClosing": true
          }
        };

        jQuery.each(buttons, function () {
          var buttonDef = this;
          if (itemConfig.forbidden_tags && itemConfig.forbidden_tags.indexOf(buttonDef.tagName) >= 0) {
            return;
          }

          var $button = createContentEditorButton($wysiwygEditor, buttonDef.title, buttonDef.tagName,
            {
              "description": buttonDef.description,
              "attributes": buttonDef.attributes,
              "selfClosing": buttonDef.selfClosing,
              "execCommand": buttonDef.command
            });
          $buttonWrapper.append($button);
          $button.on('click', function () {
            $button.data('xmleditor.handler')(jQuery(this));
            _syncWysiwygAndTextarea();
          });


        });

        $buttonWrapper.append(' <span>&nbsp;&nbsp;Add:&nbsp;</span>');

        /* add special elements button */
        buttons = [
          {
            "tagName": "a",
            "title": '<img src="' + config.basePath + '/assets/icons/link_icon.png" /> Link',
            "description": "Link",
            "selfClosing": false,
            "attributes": [
              {
                "name": "href",
                "description": "Enter the URL (e.g. http://www.google.com)"
              },
              {
                "name": "_innerHTML",
                "description": "Enter the link's text"
              },
              {
                "name": "target",
                "description": "Where should this link open?",
                "options": [
                  {"value": "_blank", "text": "In a new tab or window."},
                  {"value": "_self", "text": "In this tab or window."}
                ]
              }
            ]
          },
          "spacer",
          {
            "tagName": "inlinechoice",
            "variant": "text",
            "title": '<img src="' + config.basePath + '/assets/icons/answercontainer_text_short_icon.png" /> Answer',
            "description": "Text field where user can type answer.",
            "selfClosing": true,
            "innerHTMLAttributes": ["id"],
            "innerHTMLValue": "<input type=text />",
            "attributes": [
              {
                "name": "id",
                "description": "What id should this answer have?",
                "feedback": [
                  {
                    "correct": 0,
                    "match": "^[0-9]",
                    "text": "This id cannot start with a number.",
                    "fatal": 1,
                    "stop": 0
                  },
                  {
                    "correct": 0,
                    "match": "[^a-zA-Z_0-9]",
                    "text": "This id can only contain letters, numbers and underscores.",
                    "fatal": 1,
                    "stop": 0
                  }
                ]
              },
              {
                "name": "freeform",
                "description": "Is this answer container a select list with answer options or a text field?",
                "options": [
                  {"value": 1, "text": "Text field"}
                ],
                "hidden": true
              },
              {
                "name": "longtext",
                "description": "Should the text field be single line or multiline?",
                "options": [
                  {"value": 0, "text": "Single line (default)"},
                  {"value": 1, "text": "Multiline"}
                ]
              }
            ]
          },
          {
            "tagName": "inlinechoice",
            "variant": "select",
            "title": '<img src="' + config.basePath + '/assets/icons/answercontainer_option_icon.png" /> Answer',
            "description": "Drop down where user can select answer.",
            "selfClosing": true,
            "innerHTMLAttributes": ["id"],
            "attributes": [
              {
                "name": "id",
                "description": "What id should this answer have?",
                "feedback": [
                  {
                    "correct": 0,
                    "match": "^[0-9]",
                    "text": "This id cannot start with a number.",
                    "fatal": 1,
                    "stop": 0
                  },
                  {
                    "correct": 0,
                    "match": "[^a-zA-Z_0-9]",
                    "text": "This id can only contain letters, numbers and underscores.",
                    "fatal": 1,
                    "stop": 0
                  }
                ]
              },
              {
                "name": "freeform",
                "description": "Is this answer container a select list with answer options or a text field?",
                "options": [
                  {"value": 0, "text": "Select with answer options"},
                ],
                "hidden": true
              },
              {
                "name": "group",
                "description": "Which answer option group?",
                "options": function () {
                  var inlineOptionsAutoValues = {
                    "path": [
                      "question",
                      "inlineoption"
                    ],
                    "valueSrc": "group",
                    "titleSrc": "id"
                  };
                  var groupIds = [];
                  var groupOptions = [];
                  var inlineOptions = _getAttConfigAutoValuesChildTags(inlineOptionsAutoValues);
                  jQuery.each(inlineOptions, function (i, inlineOption) {
                    if (inlineOption.value && groupIds.indexOf(inlineOption.value) < 0) {
                      groupIds.push(inlineOption.value);
                    }
                  });

                  jQuery.each(groupIds, function (i, groupId) {
                    groupOptions.push(
                      {"value": groupId, "text": "Group " + groupId}
                    );
                  });
                  return groupOptions;
                }
              }
            ]
          },
          {
            "tagName": "mathresult",
            "title": '<img src="' + config.basePath + '/assets/icons/matheval_icon.png" /> Formula',
            "description": "Container holding the result of a mathematical formula.",
            "selfClosing": true,
            "innerHTMLAttributes": ["e", "expression"],
            "attributes": [
              {
                "name": "e",
                "help": "A formula. Examples: a=random(); a=round(a*0.8+0.11,2); min=min(round(ans,1),(ans-0.01)); etc. See documentation.",
                "description": "Enter mathematical formula:"
              }
            ]
          },
          {
            "title": '<img src="' + config.basePath + '/assets/icons/feedbackblock_icon.png" /> Feedback',
            "description": "Inline feedback block",
            "tagName": "feedbackblock",
            "selfClosing": true,
            "innerHTMLAttributes": ["id"],
            "attributes": [
              {
                "name": "id",
                "description": "What id should this feedback block have?"
              }]
          }
        ];

        // Store above definition for later use.
        $wysiwygEditor.data('xmleditor_element_definition', buttons);

        jQuery.each(buttons, function () {
          var buttonDef = this;
          if (buttonDef.toString() === 'spacer') {
            $buttonWrapper.append('<span class="xmleditor_spacer"></span>');
          }
          else {
            if (itemConfig.forbidden_tags && itemConfig.forbidden_tags.indexOf(buttonDef.tagName) >= 0) {
              return;
            }

            var $button = createContentEditorButton($wysiwygEditor, buttonDef.title, buttonDef.tagName,
              {
                "description": buttonDef.description,
                "attributes": buttonDef.attributes,
                "selfClosing": buttonDef.selfClosing,
                "innerHTMLAttributes": buttonDef.innerHTMLAttributes,
                "buttonType": 'button',
                "execCommand": buttonDef.command
              });
            $buttonWrapper.append($button);

            $button.on('click', function () {
              var $button = jQuery(this);
              if ($button.data('xmleditor.handler')) {
                $button.data('xmleditor.handler')($button);
              }
            });
          }
        });
        $buttonWrapper.append('<span class="xmleditor_spacer"></span>');
        /* add image element select */
        var $image_select;

        if (CQ_MediaModule2IsEnabled() === true) {
          $image_select = jQuery('<button type="button" class="button"><img src="' + config.basePath + '/assets/icons/media_icon.png" /> Media<small></button>');
          var mediaBrowserSettings = {"file_extensions": 'png gif jpg jpeg pdf docs xlsx doc xls ppt txt'};

          // This function will be called by Media Module media Browser after user selected media.
          var callBack = function (options) {
            var mediaInfo = options[0];

            /* Insert image placeholder */
            var src, alt, imgTagAttributes, className;
            var dataType = mediaInfo['type'];

            className = "media-element file-default media-" + mediaInfo.type;

            switch (mediaInfo.type) {
              case 'image':
                src = drupalSettings.basePath + 'closedquestion/getmedia/' + mediaInfo.fid;
                alt = mediaInfo['alt'];
                break;

              case 'video':
                src = jQuery(mediaInfo['preview']).find('img').attr('src');
                alt = mediaInfo['filename'];
                break;

              case 'document':
                src = jQuery(mediaInfo['preview']).find('img').attr('src');
                alt = mediaInfo['filename'];
                break;
            }

            imgTagAttributes = {
              "data-fid": mediaInfo.fid,
              "data-view-mode": 'default',
              "data-type": mediaInfo.type,
              "alt": alt,
              "title": alt,
              "class": className,
              "src": src
            };

            $wysiwygEditor[0].focus();
            _contentEditorInsertHTMLTag('img', '', imgTagAttributes);
            _syncWysiwygAndTextarea();

          };

          $image_select.on('click', function () {
            $wysiwygEditor[0].focus();
            $wysiwygEditor.data('saveCaretPosition')();
            Drupal.media.popups.mediaBrowser(callBack, mediaBrowserSettings);
          });

        }
        else {
          /* No Media module 2 */
          $image_select = getFileAttachmentsImageSelect({"showExternalImageOption": true});

          //add event handler to options
          $image_select.on('change', function () {
            var val = jQuery(this).children(':selected').data('fileplaceholder');

            if (val !== undefined) {
              _contentEditorInsertHTMLTag('span', val, {"class": "image"});
            }

            $image_select.val('-1');
            _syncWysiwygAndTextarea();
          });
        }

        $buttonWrapper.append($image_select);

        /* Add table options */
        $buttonWrapper.append('<span>&nbsp;&nbsp;Table:&nbsp;</span>');
        var $addTableButton = jQuery('<button class="button" type="button" title="Add table"><img src="' + config.basePath + '/assets/icons/table_add_icon.png" title="Add table"/> Add</button>');
        var $addRowButton = jQuery('<button style="display:none" class="button" type="button" title="Add row"><img src="' + config.basePath + '/assets/icons/row_add_icon.png" title="Add row"/></button>');
        var $delRowButton = jQuery('<button style="display:none" class="button" type="button" title="Remove row"><img src="' + config.basePath + '/assets/icons/row_del_icon.png" title="Remove row"/></button>');
        var $addColButton = jQuery('<button style="display:none" class="button" type="button" title="Add column"><img src="' + config.basePath + '/assets/icons/col_add_icon.png" title="Add column"/></button>');
        var $delColButton = jQuery('<button style="display:none" class="button" type="button" title="Remove column"><img src="' + config.basePath + '/assets/icons/col_del_icon.png" title="Remove column"/></button>');
        var $toggleHeaderCellButton = jQuery('<button style="display:none" class="button" type="button" title="Turn a cell into a header cell and vice versa"><img src="' + config.basePath + '/assets/icons/table_header_toggle_icon.png" title="Turn a cell into a header cell and vice versa"/></button>');
        $buttonWrapper.append($addTableButton, $addRowButton, $delRowButton, $addColButton, $delColButton, $toggleHeaderCellButton);

        //init table edit plugin
        $wysiwygEditor.simpleTableEdit();

        //show/hide buttons when user in/out table.
        $wysiwygEditor.on('keyup click', function () {
          if ($wysiwygEditor.data('simpleTableEdit').inTable() === true) {
            $addTableButton.hide();
            $addRowButton.show();
            $delRowButton.show();
            $addColButton.show();
            $delColButton.show();
            $toggleHeaderCellButton.show();
          }
          else {
            $addTableButton.show();
            $addRowButton.hide();
            $delRowButton.hide();
            $addColButton.hide();
            $delColButton.hide();
            $toggleHeaderCellButton.hide();
          }
        });

        $addTableButton.on('click', function () {
          $wysiwygEditor[0].focus();
          $wysiwygEditor.data('simpleTableEdit').addTable();
          _syncWysiwygAndTextarea();
        });

        $addRowButton.on('click', function () {
          $wysiwygEditor[0].focus();
          $wysiwygEditor.data('simpleTableEdit').addRow();
          _syncWysiwygAndTextarea();
        });

        $delRowButton.on('click', function () {
          $wysiwygEditor[0].focus();
          $wysiwygEditor.data('simpleTableEdit').removeRow();
          _syncWysiwygAndTextarea();
        });

        $addColButton.on('click', function () {
          $wysiwygEditor[0].focus();
          $wysiwygEditor.data('simpleTableEdit').addColumn();
          _syncWysiwygAndTextarea();
        });

        $delColButton.on('click', function () {
          $wysiwygEditor[0].focus();
          $wysiwygEditor.data('simpleTableEdit').removeColumn();
          _syncWysiwygAndTextarea();
        });

        $toggleHeaderCellButton.on('click', function () {
          $wysiwygEditor[0].focus();
          $wysiwygEditor.data('simpleTableEdit').toggleHeaderCell();
          _syncWysiwygAndTextarea();
        });

        /* Append elements to DOM.
         */
        $buttonWrapper.append($toggleCodeButton);
        $editorWrapper.append($wysiwygEditor);

        $editorWrapper.append($textarea);
        _syncWysiwygAndTextarea();
        $wrapper.append('<div class="xmlEditorAttributeFeedback"></div>');
        $textarea.val(data.content);
        _syncWysiwygAndTextarea(true);

        // Add image resize.
        jQuery($wysiwygEditor).webkitimageresize({
          "beforeElementSelect": function (img) {
            if (jQuery(img).hasClass('xmlEditorIcon')) {
              // Don't select icons added by wysiwyg in editor.
              return false;
            }
          },
          "afterResize": function () {
            _syncWysiwygAndTextarea();
          }
        });
      }
      else {
        var $wrapper = jQuery("<th class='xmlJsonEditor_attribute_title'>Content:</th>");
        var $wrapper_wrapper = jQuery('<tr class="xmlJsonEditor_attribute xmlJsonEditor_attribute_container"></tr>');
        editor.append($wrapper_wrapper);
        $wrapper_wrapper.append($wrapper);
        $editorWrapper = jQuery('<td class="xmlJsonEditor_attribute_container_wrap" />');
        $wrapper_wrapper.append($editorWrapper);
        var $textarea = jQuery("<input type='text' name='cq_editor_content' id='cq_editor_content' />")
          .val(data.content);
        $editorWrapper.append($textarea);
      }

      saveHandlers.push(
        function (element) {
          return function (e) {
            var content = $textarea.val();
            element.data().jstree_cq.content = content;
            notifyListeners('change', {
              "element": element,
              "what": "content"
            });
          };
        }(element)
        );
    }

    function _contentEditorInsertHTMLTag(tagName, innerText, attributes) {
      /* Get selection */
      var sel = window.getSelection();
      var range = sel.getRangeAt(0);
      attributes = attributes === undefined ? {} : attributes;

      /* Create tag with attributes */
      var frag = document.createDocumentFragment();
      var el = document.createElement(tagName);
      if (innerText) {
        el.innerHTML = innerText;
      }

      jQuery.each(attributes, function (name, value) {
        if (name) {
          el.setAttribute(name, value);
        }
      });

      /* Insert it in DOM */
      frag.appendChild(el);
      range.insertNode(document.createTextNode('\u200B')); //Add these to fix not-able-to-put-cursor-right-after-element bug in FF
      range.insertNode(frag);
      range.insertNode(document.createTextNode('\u200B'));
    }

    /**
     * Creates a button for the content editor.
     *
     * @param {object} $wysiwygEditor
     *   The wysiwyg editor instance.
     * @param {string} title
     *   The title of the button.
     * @param {string} tagName
     *   The tag.
     * @param {object} options
     *   options.description: A description for the end user.
     *   options.tagName: The tag this button should insert.
     *   options.attributes: The attributes config for the tag
     *   options.selfClosing: Whether the tag is self-closing (e.g. <br />). Default: false.
     *   options.innerHTMLAttributes: Array of attributes to be used as innerHTML to show state to end-user.
     *   options.buttonType: The HTML tag representing this button. Default: <button>
     *   options.execCommand: Optional: an execCommand, see https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
     * @returns {object}
     *   A jQuery object holding the button.
     */
    function createContentEditorButton($wysiwygEditor, title, tagName, options) {
      var description = options.description;
      var attributes = options.attributes;
      var innerHTMLAttributes = options.innerHTMLAttributes === undefined ? [] : options.innerHTMLAttributes;
      var buttonType = options.buttonType !== undefined ? options.buttonType : 'button';
      var execCommand = options.execCommand;

      var $button = jQuery('<' + buttonType + ' class="button ' + tagName + '" type="button" title="' + description + '">' + title + '</' + buttonType + '>');
      var handler;

      /* Insert simple button commands, e.g. for italic, bold, etc. */
      if (execCommand) {
        handler = function (e) {
          document.execCommand(execCommand, false, null);
          return false;
        };
      }

      /* Insert tag button */
      if (attributes !== undefined) {
        handler = function (e) {
          var promptHtmlElements = _getPopupModalHTMLElements(attributes);
          $wysiwygEditor.data('saveCaretPosition')(); //Save selection as modal might steal it..

          /* Create modal window with user input. */
          popupModal(promptHtmlElements, function (formValues) {
            /* Form values will contain all attributes and the _innerHTML property */
            var outerHTMLAttributes = {};
            var innerHTML = false;
            var is_special_element;

            jQuery.each(formValues, function (name, value) {
              if (name !== '_innerHTML') {
                outerHTMLAttributes[name] = value;
              }
              else {
                innerHTML = value;
              }
            });

            // Add special attributes for special elements
            is_special_element = $wysiwygEditor.data('xmleditor_element_definition').find(function (element) {
              return (element.tagName === tagName && tagName !== 'a');
            });
            if (is_special_element) {
              innerHTML = _getSpecialElementInnerHTML(title, outerHTMLAttributes, innerHTMLAttributes);
              outerHTMLAttributes['unselectable'] = "on";
              outerHTMLAttributes['contenteditable'] = "false";
            }

            // Insert tag.
            $wysiwygEditor.data('restoreCaretPosition')(); // Restore selection.
            _contentEditorInsertHTMLTag(tagName, innerHTML, outerHTMLAttributes);
            $wysiwygEditor.trigger('keyup'); // Trigger sync with textarea.
          });

          return false;
        };
      }

      $button.data('xmleditor.handler', handler);

      return $button;
    }

    /**
     * Helper function to create inner HTML of special tags.
     *
     * @param {string} title
     * @param {object} outerHTMLAttributes
     *   Key-values of the special tag's attributes.
     *
     * @param {array} innerHTMLAttributes
     *   Array with attribute names which should form the innerhtml of the special tag.
     *
     * @returns {String}
     */
    function _getSpecialElementInnerHTML(title, outerHTMLAttributes, innerHTMLAttributes) {
      if (!title) {
        return '';
      }
      outerHTMLAttributes = outerHTMLAttributes ? outerHTMLAttributes : {};
      innerHTMLAttributes = innerHTMLAttributes ? innerHTMLAttributes : {};

      var innerHTMLArr = [];
      jQuery.each(innerHTMLAttributes, function (i, att) {
        if (typeof outerHTMLAttributes[att] === 'string') {
          innerHTMLArr.push(outerHTMLAttributes[att]);
        }
      });
      var filteredTitle = jQuery(title)
        .filter('img')
        .attr('onmousedown', 'if (event.preventDefault) event.preventDefault()')
        .addClass('xmlEditorIcon')[0]
        .outerHTML;
      return '<small>' + filteredTitle + '</small><span>' + innerHTMLArr.join(', ') + '</span>';
    }

    /**
     * Checks whether element is special element.
     * @param {string} tagName
     * @param {object} outerHTMLAttributes
     * @param {object} element_definition
     * @returns {Boolean}
     */
    function _doSpecialElementCheck(tagName, outerHTMLAttributes, element_definition) {
      if (tagName === 'inlinechoice') {
        if (outerHTMLAttributes && parseInt(outerHTMLAttributes['freeform'], 10) === 1 && element_definition.variant !== 'text') {
          return false;
        }
      }
      if (tagName === 'a') {
        return false;
      }
      return true;
    }

    /**
     * Prepares an array with HTML element definitions the popupModal function
     * likes.
     *
     * @param {object} attributes
     * @returns {array}
     */
    function _getPopupModalHTMLElements(attributes) {
      var promptHtmlElements = [];

      jQuery.each(attributes, function () {
        var description = this.description === undefined ? 'Enter the value for ' + this.name : this.description;
        var formElementDefinition = {};
        promptHtmlElements.push({
          "type": "text",
          "value": Drupal.t(description),
          "hidden": this.hidden
        });

        if (this.options) {
          formElementDefinition = {
            "type": "select",
            "name": this.name,
            "options": this.options
          };
        }
        else {
          formElementDefinition = {
            "type": "input",
            "name": this.name
          };
        }

        formElementDefinition.hidden = this.hidden;

        if (this.onchange) {
          formElementDefinition.onchange = this.onchange;
        }
        if (this.feedback) {
          formElementDefinition.feedback = this.feedback;
        }
        if (this.value !== undefined) {
          formElementDefinition.value = this.value;
        }

        // Add form element.
        promptHtmlElements.push(formElementDefinition);

        // Add help text.
        if (this.help !== undefined) {
          promptHtmlElements.push({
            "type": "text",
            "value": '<div class="xmlEditorDialogHelp">' + Drupal.t(this.help) + '</div>',
            "hidden": formElementDefinition.hidden
          });
        }
      });
      return promptHtmlElements;
    }

    /**
     * Creates a nice prompt using jQuery UI.
     *
     * @param {array} htmlElements
     *   Collection of html elements.
     *   {
     *     "type": (string) text|input|select,
     *     "name": (string)
     *     "value": (string)
     *     "options": [{"text": (string), "value": (string}],
     *     "hidden": (boolean)
     *   }
     *
     * @param {type} callBack
     *   Function to callback when user presses Ok button. This function will
     *   receive all form values in an object with as name-value pairs.
     * @returns {undefined}
     */
    function popupModal(htmlElements, callBack, showDeleteButton) {
      var i, j, htmlElement, option, selectedAttr, disabledAttr, $elementWrapper, $formElement;
      var $dialogContainer = jQuery('<div class="xmlEditorDialog" />');
      var $feedbackElement = jQuery('<div class="xmlEditorAttributeFeedback"></div>');
      for (i = 0; i < htmlElements.length; i++) {
        htmlElement = htmlElements[i];

        switch (htmlElement.type) {
          case 'text':
            $elementWrapper = jQuery('<div>' + htmlElement.value + '</div>');
            break;

          case 'input':
            $elementWrapper = jQuery('<div />');
            $formElement = jQuery('<input name="' + htmlElement.name + '" value="' + (htmlElement.value ? htmlElement.value : "") + '" />');
            $elementWrapper.append($formElement);
            $elementWrapper.append($feedbackElement);
            if (htmlElement.feedback) {
              var handleKeyup = function ($formElement, feedback) {
                $formElement.bind('keyup', function () {
                  var $okButton = jQuery('.ui-dialog-buttonset .okButton');

                  // First reset
                  $formElement.removeClass('xmlEditorAttributeFeedback_error');
                  $okButton.show();
                  $feedbackElement.html('');

                  // Check for and show feedback
                  for (j = 0; j < feedback.length; j++) {
                    if (new RegExp(feedback[j].match).test($formElement.val()) === true) {
                      $feedbackElement.append(feedback[j].text);
                      $formElement.addClass('xmlEditorAttributeFeedback_error');
                      $okButton.hide();
                    }
                  }
                });
              };

              handleKeyup($formElement, htmlElement.feedback);
            }
            break;

          case 'select':
            $elementWrapper = jQuery('<div />');
            if (typeof htmlElement.options === 'function') {
              htmlElement.options = htmlElement.options();
            }
            htmlElement.value = htmlElement.value === undefined ? '' : htmlElement.value;

            disabledAttr = (htmlElement.options && htmlElement.options.length === 0) ? ' disabled' : '';
            $formElement = jQuery('<select name="' + htmlElement.name + '"' + disabledAttr + ' />');

            for (j = 0; j < htmlElement.options.length; j++) {
              option = htmlElement.options[j];
              selectedAttr = option.value.toString() === htmlElement.value.toString() ? ' selected' : '';
              $formElement.append('<option value="' + option.value + '"' + selectedAttr + '>' + option.text + '</option>');
            }

            $elementWrapper.append($formElement);

            break;
        }

        if (htmlElement.onchange) {
          $formElement.bind('change', htmlElement.onchange);
        }
        if (htmlElement.hidden === true) {
          $elementWrapper.css('display', 'none');
        }
        $dialogContainer.append($elementWrapper);

      }

      var dialogButtons = [
        {
          "text": "Ok",
          "class": "okButton",
          "click": function () {
            var formValues = {};
            var allHaveValue = true;
            jQuery(":input:enabled", this).each(function () {
              var $input = jQuery(this);
              var value = $input.val();
              allHaveValue = (value !== '' && value !== undefined && value !== null) ? allHaveValue : false;
              formValues[$input.attr('name')] = value;
            });
            if (allHaveValue) {
              callBack(formValues);
              jQuery(this).dialog("close");
            }
            else {
              alert(Drupal.t('Please fill out the complete form.'));
            }
          }
        },
        {
          "text": "Cancel",
          "click": function () {
            jQuery(this).dialog("close");
          }
        }
      ];

      if (showDeleteButton && showDeleteButton === true) {
        dialogButtons.push({
          "click": function () {
            callBack('delete');
            jQuery(this).dialog("close");
          },
          "class": 'deleteButton'
        });
      }

      jQuery($dialogContainer).dialog({
        "resizable": false,
        "buttons": dialogButtons,
        "open": function () {
          // Add icon to delete button.
          jQuery(this).parent().find('.deleteButton').html('<i class="fa fa-trash fa-lg"></i>');

          // Trigger change event on all form elements in modal.
          jQuery(this).find(':input').trigger('change');
        }

      });
    }

    /**
     * Creates and set a title for the element.
     *
     * @param element
     *   The LI tree element of the item that needs a title.
     */
    function createTitleForElement(element, elementConfig) {
      var data = element.data().jstree_cq;
      var children = tree.jstree_cq("core")._get_children(element);
      var childData = [];
      for (var i = 0; i < children.length; i++) {
        childData.push(jQuery(children[i]).data().jstree_cq);
      }
      return createTitleForData(data, childData, element, elementConfig);
    }

    /**
     * Returns the attributes of a jQuery-fied DOM element.
     *
     * @param {object} $node
     * @returns {object}
     */
    function getDomElementAttributes($node) {
      var attrs = {};
      jQuery.each($node[0].attributes, function (index, attribute) {
        attrs[attribute.name] = attribute.value;
      });
      return attrs;
    }

    /**
     * Updates the form element feedback
     * @param formElement
     *   DOM-node
     * @param feedbackId
     *   An unique id for this feedback (so it can be removed later on)
     * @param feedbackObject
     *   An object, {"type": string, "text": string} in which "type" is the
     *   feedback type; the function will add a class
     *   xmlEditorAttributeFeedback_<type> to both the form element and the <li>
     *   containing the "text"
     */
    function updateFormElementFeedback(formElement, feedbackId, feedbackObject) {
      formElement = jQuery(formElement);
      feedbackObject = feedbackObject || {};
      var feedbackMessage = feedbackObject.text;
      var feedbackType = feedbackObject.type;
      var ul = jQuery('<ul></ul>');
      /* get feedback object from form element */
      var feedbackWrapper = formElement.closest('.xmlJsonEditor_attribute').find('.xmlEditorAttributeFeedback');
      var formElementFeedback = formElement.data('xmlJsonEditor.feedback') || {};
      if (!feedbackMessage) {
        /* remove class from form element */
        if (formElementFeedback[feedbackId] && formElementFeedback[feedbackId].type) {

          formElement.removeClass('xmlEditorAttributeFeedback_' + formElementFeedback[feedbackId].type);
        }

        /* delete feedback object */
        delete formElementFeedback[feedbackId];
      }
      else {
        /* add feedback object */
        if (!formElementFeedback[feedbackId]) {
          formElementFeedback[feedbackId] = {};
        }

        formElementFeedback[feedbackId].text = jQuery('<li class="xmlJsonEditor_feedback_' + feedbackType + '">' + feedbackMessage + '</li>');
        formElementFeedback[feedbackId].type = feedbackType;
      }

      /* put feedback in $wrapper */
      for (feedbackId in formElementFeedback) {
        ul.append(formElementFeedback[feedbackId].text);
        formElement.addClass('xmlEditorAttributeFeedback_' + formElementFeedback[feedbackId].type);
      }
      feedbackWrapper.empty();
      feedbackWrapper.append(ul);
      /* store feedback object in form element */
      formElement.data('xmlJsonEditor.feedback', formElementFeedback);
    }

    /**
     * Creates and set a title for the dataset of an element.
     *
     * @param data
     *   The data of an LI tree element of the item that needs a title.
     * @param children
     *   An array of data of the child LI tree elements of the item that needs
     *   a title.
     * @param contextElement
     *   The LI tree element to create the title for.
     * @param typeConfig
     *   The config return by getConfig for this node.
     */
    function createTitleForData(data, children, contextElement, typeConfig) {
      var type = data.type;
      typeConfig = typeConfig !== undefined ? typeConfig : getConfig(type, contextElement);
      var content = data.content;
      var title;
      var shortened;
      var oldLength;

      if (typeConfig.short_title !== undefined) {
        title = typeConfig.short_title;
      }
      else if (typeConfig.title !== undefined) {
        title = typeConfig.title;
      }
      else {
        title = _ucfirst(type);
      }

      if (title.length > 0) {
        title = '<em>' + title + '</em>';
      }

      // Add attribute values to title (if configured)
      if (typeConfig.atts_in_title !== undefined) {
        for (var attId in typeConfig.atts_in_title) {
          var attName = typeConfig.atts_in_title[attId];
          var attValue = data.attributes[attName];
          var autoValueChildTags = false;
          var autoValueChildTagsByValue = {};

          // Check for values/auto_values, so we can show them in the tree in stead of ids.
          if (typeConfig.attributes[attName] && typeConfig.attributes[attName].values) {
            autoValueChildTags = typeConfig.attributes[attName].values;
            autoValueChildTagsByValue = {};
            jQuery.each(autoValueChildTags, function (i, avcTag) {
              autoValueChildTagsByValue[avcTag.value] = avcTag.title;
            });
          }
          if (typeConfig.attributes[attName] && typeConfig.attributes[attName].auto_values) {
            autoValueChildTags = _getAttConfigAutoValuesChildTags(typeConfig.attributes[attName].auto_values);
            autoValueChildTagsByValue = {};

            jQuery.each(autoValueChildTags, function (i, avcTag) {
              autoValueChildTagsByValue[avcTag.value] = avcTag.title + (avcTag.group ? ' (group ' + avcTag.group + ')' : '');
            });
          }

          // Get string to be shown in tree.
          var attTitle = (typeConfig.attributes[attName] && typeof typeConfig.attributes[attName].short_title !== 'undefined') ? typeConfig.attributes[attName].short_title : attName;

          if (autoValueChildTags) {
            attValue = autoValueChildTagsByValue[attValue];
          }
          if (attValue !== undefined) {
            title = title + ' <var>' + attTitle + '</var> ' + ' <code>' + attValue + '</code>';
          }
        }
      }

      // Add content to title (if configured)
      if (typeConfig.content !== undefined && typeConfig.content) {
        var myRexexp = new RegExp("<[/]?[^<>]*>", "g");
        // Strip out double spacings.
        shortened = content.replace(/(\t|\n|\r)/g, " ");
        do {
          oldLength = shortened.length;
          shortened = shortened.replace("  ", " ");
        } while (shortened.length !== oldLength);
        // We have an element with content. Show the first bit of content.
        title = title + (title.length > 0 ? "; " : "") + '<code>' + shortened.replace(myRexexp, " ") + '</code>';
      }
      else if (children !== undefined && typeConfig.children_in_editor !== undefined) {
        for (var i = 0; i < typeConfig.children_in_editor.length; i++) {
          var childType = typeConfig.children_in_editor[i];
          var childConfig = getConfig(childType, contextElement);
          if (childConfig.content !== undefined && childConfig.content) {
            for (var j = 0; j < children.length; j++) {
              if (children[j].metadata) {
                var childMeta = children[j].metadata;
              }
              else {
                childMeta = children[j];
              }
              if (childMeta.type === childType) {

                // Strip out double spacings.
                shortened = childMeta.content.replace(/(\t|\n|\r)/g, " ");
                do {
                  oldLength = shortened.length;
                  shortened = shortened.replace("  ", " ");
                } while (shortened.length !== oldLength);
                myRexexp = new RegExp("<[/]?[^<>]*>", "g");
                // We have an element with content. Show the first bit of content.
                title = title + (title.length > 0 ? "; " : "") + '<code>' + shortened.replace(myRexexp, " ") + '</code>';
                break;
              }
            }
          }
        }
      }




      title = title.replace(/\[\[\{[^\]]*\]\]*/ig, '[media]'); // Remove Media tags from title.
//      title = title.length > 70 ? title.substring(0, 67) + "..." : title; //Prevent long titles.

      return title;
    }

    /**
     * Clear out the content of the editor divs.
     */
    function emptyEditor() {
      var config = getConfig();
      saveHandlers = [];
      var editor = jQuery(editorSelector);
      editor.find(".xmlJsonEditor_form_description").remove();
      editor.find("#editor_values_contents").empty();
      editor.find("#selectedNodeAddlist").children().remove();
      editor.find('#editor_values').children().filter('.xmlJsonEditor_form_description').remove();
      editor.find('#selectedNodeDescription').empty();
      // If the tree is empty, show the root node items in the add dropdown.
      if (treeIsEmpty()) {
        var addNodeOptions = getAddNodeOptions(config.valid_children);
        var addButtonWrapper = jQuery('#selectedNodeAddlist');
        jQuery(addNodeOptions).each(function () {
          addButtonWrapper.append(this);
        });
      }
    }

    /**
     * Checks if the tree is empty.
     *
     * @return true if the tree is empty, false otherwise.
     */
    function treeIsEmpty() {
      var container = tree.jstree_cq("core").get_container();
      var root = container[0].children[0].children[0];
      var treeRoot = tree.jstree_cq("core")._get_node(root);
      return (treeRoot.length === 0);
    }

    /**
     * Converts a closedQuestion XML string to a jstree_cq tree object.
     * @param {string} xmlString
     *   The string to convert.
     */
    function questionStringToTreeObject(xmlString) {
      var xmlDoc1 = parseXml(xmlString);
      var data;
      var count = xmlDoc1.childNodes.length;
      for (var n = 0; n < count; n++) {
        var child = xmlDoc1.childNodes[n];
        if (child.nodeType === 1) {
          data = handleNode(child);
        }
      }
      return data;
    }

    /**
     * Adds the attributes of the DOM element to the target jstree_cq object.
     *
     * @param {object} nodeConfig
     *   Edito config for the node.
     * @param {object} node
     *   DOM node.
     * @param {object} target
     *   jstree_cq object.
     * @returns {undefined}
     */
    function parseAttributes(nodeConfig, node, target) {
      var count = node.attributes.length;
      for (var n = 0; n < count; n++) {
        var attr = node.attributes[n];
        var name = attr.nodeName.toLowerCase();
        var value = attr.nodeValue;
        var attConfig = nodeConfig.attributes[name];
        if (attConfig === undefined) {
//          console.log(Drupal.t('An unknown parameter "@item" was found in an item of type "@node".\nIf you do not know what to do, contact your technical support for further assitance.', {
//            "@item": name,
//            "@node": node.nodeName
//          }));
        }
        else {
          if (attConfig.alias_of !== undefined) {
            name = attConfig.alias_of;
            attConfig = nodeConfig.attributes[name];
          }
          if (attConfig.deprecated === 1) {
//            console.log(Drupal.t('A deprecated parameter "@item" was found in an item of type "@node".\nPlease check all items of this type in the tree for information on how to fix this.', {
//              "@item": name,
//              "@node": node.nodeName
//            }));
          }
          if (attConfig.value_aliases !== undefined) {
            var aliasOf = attConfig.value_aliases[value.toLowerCase()];
            if (aliasOf !== undefined) {
              value = aliasOf;
            }
          }
        }
        target.metadata.attributes[name] = value;
      }
    }

    /**
     * Creates branch for grouping together similar items.
     *
     * @param type
     *   The type of the group.
     * @param title
     *   The title of the group.
     *
     * @return
     *   A json tree-branch.
     */
    function createGroup(type, title) {
      var group = {
        "data": {
          "title": '<em>' + title + '</em>',
          "icon": "folder"
        },
        "state": "open",
        "attr": {
          "rel": type
        },
        "metadata": {
          "type": type,
          "attributes": {},
          "content": ""
        },
        "children": []
      };
      return group;
    }

    /**
     * Adds the chilren of the DOM element to the target jstree_cq object.
     * @param {object} node
     *   The XML node.
     * @param {object} target
     *   The jstree_cq object.
     */
    function parseChildren(node, target) {
      var config = getConfig();
      var groups = {};
      target.children = [];
      var count = node.childNodes.length;
      for (var n = 0; n < count; n++) {
        var child = node.childNodes[n];
        switch (child.nodeType) {
          case 1:
            var data = handleNode(child);
            var childConfig = config.types[data.metadata.type];
            if (childConfig === undefined) {
              alert(Drupal.t("Unknown child: ") + data.metadata.type);
              target.children.push(data);
            }
            else {
              if (childConfig.in_group === undefined) {
                target.children.push(data);
              }
              else {
                var groupName = childConfig.in_group;
                if (groups[groupName] === undefined) {
                  var groupConfig = config.types[groupName];
                  var groupTitle = groupConfig.title === undefined ? groupName : groupConfig.title;
                  groups[groupName] = createGroup(groupName, groupTitle);
                  target.children.push(groups[groupName]);
                }
                var group = groups[groupName];
                group.children.push(data);
              }
            }
            break;
        }
      }
    }

    /**
     * Turn the XML node into a jstree_cq tree element.
     *
     * @param {object} node
     *   The XML node.
     */
    function handleNode(node) {
      var target = null;
      var tagName = node.tagName.toLowerCase();
      var nodeConfig = getConfig(tagName);
      var nodeId = "xmlEditor_" + (nextId++);
      if (nodeConfig === undefined) {
        return {
          "data": {
            "title": "UNKNOWN: " + tagName
          },
          "attr": {
            "rel": tagName,
            "id": nodeId
          },
          "metadata": {
            "type": tagName,
            "attributes": {},
            "content": ""
          }
        };
      }

      if (nodeConfig.alias_of !== undefined) {
        tagName = nodeConfig.alias_of;
        if (nodeConfig.content_to_attribute !== undefined) {
          node.setAttribute(nodeConfig.content_to_attribute, getXMLNodeInnerHTML(node));
        }

        nodeConfig = getConfig(tagName);
      }

      /* do some configs */
      target = {
        "data": {
          "title": tagName
        },
        "attr": {
          "rel": tagName,
          "id": nodeId
        },
        "metadata": {
          "type": tagName,
          "attributes": {},
          "content": ""
        }
      };
      //hide node in tree?
      if (nodeConfig.hidden !== undefined && nodeConfig.hidden === 1) {
        target.attr.style = "display: none";
      }

      //parse attributes
      parseAttributes(nodeConfig, node, target);
      //do some settings
      if (nodeConfig.max_children === 0) {
        if (nodeConfig.content) {
          var content = getXMLNodeInnerHTML(node);
          target.metadata.content = content;
        }
      }
      else {
        if (nodeConfig.state === undefined) {
          target.state = "open";
        }
        else {
          target.state = nodeConfig.state;
        }
        parseChildren(node, target);
      }

      // Now we set a pretty title.
      target.data.title = createTitleForData(target.metadata, target.children, null, nodeConfig);
      return target;
    }

    /**
     * Converts XML string to XML DOM.
     *
     * @param {string} xml
     *   The XML as a string.
     * @credits http://goessner.net/download/prj/jsonxml/
     */
    function parseXml(xml) {
      var dom = null;
      if (window.DOMParser) {
        try {
          dom = (new DOMParser()).parseFromString(xml, "text/xml");
        } catch (e) {
          dom = null;
        }
      }
      else if (window.ActiveXObject) {
        try {
          dom = new ActiveXObject('Microsoft.XMLDOM');
          dom.async = false;
          if (!dom.loadXML(xml)) { // parse error ..
            window.alert(dom.parseError.reason + dom.parseError.srcText);
          }
        } catch (e) {
          dom = null;
        }
      }
      else {
        alert(Drupal.t("cannot parse xml string!"));
      }
      return dom;
    }

    /**
     * Returns serialized XML
     *
     * @param xmlObject
     *   A xml object
     *
     * @return string
     */
    function getSerializedXML(xmlObject) {
      var serializer;
      var serialized;
      try {
        // XMLSerializer exists in current Mozilla browsers
        serializer = new XMLSerializer();
        serialized = serializer.serializeToString(xmlObject);
      } catch (e) {
        // Internet Explorer has a different approach to serializing XML
        serialized = xmlObject.xml;
      }

      return serialized;
    }

    /**
     * Convert a string to the content of a node.
     *
     * @param {string} xmlString
     * @param {object} targetNode
     */
    function InnerHTMLToNode(xmlString, targetNode) {
      var dom = null;
      var xmlFromString = loadXMLFromString(xmlString, true);
      if (xmlFromString.success === false) {
        dom = targetNode.ownerDocument;
        var textNode = dom.createCDATASection(xmlString);
        targetNode.appendChild(textNode);
      }
      else {
        dom = xmlFromString.dom;
        var count = dom.childNodes[0].childNodes.length;
        for (var n = 0; n < count; n++) {
          var child = dom.childNodes[0].childNodes[n];
          targetNode.appendChild(child.cloneNode(true));
        }
      }
    }

    /**
     * Converts XML string to XML DOM object
     * @param xmlString
     *   The string to convert to XML.
     * @param isInnerXML
     *  Boolean determining whether the xmlString is a full xml string or an
     *  inner xml string.
     *
     * @return object
     *   Object with the fields:
     *   - success: boolean Indication if the conversion was successful.
     *   - dom: XML-DOM object of the given XML.
     *   - errorMessage: string Message if the conversion failed.
     */
    function loadXMLFromString(xmlString, isInnerXML) {
      var dom = null;
      var returnObject = {};
      var errorMsg;
      if (isInnerXML) {
        xmlString = "<root>" + xmlString + "</root>";
      }

      if (window.DOMParser) {
        try {
          dom = (new DOMParser()).parseFromString(xmlString, "text/xml");
          if (dom.documentElement.nodeName === "parsererror" || dom.documentElement.firstChild.nodeName === "parsererror") {
            errorMsg = dom.documentElement.firstChild.textContent;
            dom = null;
          }
        } catch (e) {
          dom = null;
          errorMsg = "Markup error detected in the text. Check if tags are closed properly (<p>...</p>) and if no strange symbols are present.";
        }
      }
      else if (window.ActiveXObject) {
        try {
          dom = new ActiveXObject('Microsoft.XMLDOM');
          dom.async = false;
          if (!dom.loadXML(xmlString)) {
            errorMsg = dom.parseError.reason + dom.parseError.srcText;
            dom = null;
          }
        } catch (e) {
          dom = null;
          errorMsg = dom.parseError.reason + dom.parseError.srcText;
        }
      }
      else {
        errorMsg = Drupal.t("cannot parse xml string!");
      }

      if (dom !== null) {
        returnObject.success = true;
        returnObject.dom = dom;
      }
      else {
        returnObject.success = false;
        returnObject.errorMessage = errorMsg;
      }

      return returnObject;
    }


    /**
     * Convert the content of the node into a string.
     *
     * @param {object} node
     *   XML node.
     */
    function getXMLNodeInnerHTML(node) {
      if (node.childNodes.length === 0) {
        // The node has no children to return!
        return "";
      }
      else if (node.childNodes.length === 1 && node.childNodes[0].nodeType === 4) {
        // The node has 1 child of type CDATA
        return node.childNodes[0].data;
      }
      var tagName = node.tagName.toLowerCase();
      var myregexp = new RegExp("<[\/]?(" + tagName + ")[^><]*>", "i");
      var serialized = getSerializedXML(node);
      var shorter = serialized.substr(0, serialized.length - tagName.length - 3).replace(myregexp, "");
      return shorter;
    }

    /**
     * Checks if the attributes object contains all mandatory attributes and
     * adds them if needed.
     *
     * @param attributes
     *   Object containing the attributes of a node.
     * @param attributeConfig
     *   Object containing the attribute configuration of the node.
     */
    function checkMandatoryAttributes(attributes, attributeConfig) {
      for (var attName in attributeConfig) {
        var attConfig = attributeConfig[attName];
        if (attConfig.mandatory !== undefined && attributes[attName] === undefined) {
          attributes[attName] = attConfig.mandatory;
        }
      }
    }

    /**
     * Adds a node to the tree
     *
     * @param type
     *   String (optional) determining the type, as defined in the config.
     * @param parent
     *   DOM/Jquery node (optional) The parent to which the new node will be
     *   added.
     * @param attributes
     *   JSON attribute name/values
     * @see http://www.jstree_cq.com/documentation/crrm
     */
    function addNode(type, parent, attributes) {
      if (!type) {
        var $addChildButton = jQuery('#selectedNodeAddlist').children('.selected');
        type = $addChildButton.attr('data-value');
      }

      parent = parent || tree.jstree_cq('get_selected'); //currently selected node;
      var group_element;
      attributes = attributes || {};
      var nodeId = "xmlEditor_" + (nextId++);
      var position = "last"; //it will be added as last child
      if (treeIsEmpty()) {
        position = 'before';
        parent = -1;
      }
      var newElementConfig = getConfig(type, parent); //@todo: check whether this will not lead to bugs, as the config of the new node is obtained in its parent's context

      if (typeof newElementConfig.in_group === 'string') {
        // New element should belong in group.
        group_element = tree.xmlTreeEditor('search', newElementConfig.in_group)[0];

        if (group_element === undefined) {
          // No group found, first create it.
          addNode(newElementConfig.in_group, parent);

          // Get group element and set it as parent.
          parent = tree.xmlTreeEditor('search', newElementConfig.in_group)[0];
        }
        else {
          parent = group_element;
        }
      }
      var title = _ucfirst(newElementConfig.title) || _ucfirst(type);
      var newNodeConfig = {
        "attr": {
          "rel": type,
          "id": nodeId
        },
        "data": {
          "title": title
        }
      };
      // Check attributes
      if (newElementConfig.attributes) {
        checkMandatoryAttributes(attributes, newElementConfig.attributes);
      }

      // hide node in tree?
      if (newElementConfig.hidden !== undefined && newElementConfig.hidden === 1) {
        newNodeConfig.attr.style = "display: none";
      }

      var callback = function () {

        var $addChildButton = jQuery('#selectedNodeAddlist').children('.selected');
        var i, newElement = arguments[0], $newElement = jQuery(newElement);
        var auto_children = newElementConfig.auto_children;
        jQuery(newElement).data("jstree_cq", {
          "type": type,
          "attributes": attributes,
          "content": ""
        });
        _setTreeElementClass($newElement);
        /* give the element a proper title */
        tree.jstree_cq("rename_node", newElement, createTitleForElement(newElement));
        /* add auto children */
        if (auto_children) {
          for (i = 0; i < auto_children.length; i++) {
            addNode(auto_children[i], newElement);
          }
        }

        /* highlight the new element */
        var $addChildButtonClone = $addChildButton.clone();
        var addChildButtonOffset = $addChildButton.offset();
        var newElementOffset = $newElement.offset();
        var newElementHeight = $newElement.height();
        $newElement.hide();
        $addChildButtonClone.css({'background': '#efe', 'position': 'absolute'});
        $addChildButtonClone.addClass('selectedNodeAddlistLi');
        jQuery('body').append($addChildButtonClone);
        $addChildButtonClone.offset(addChildButtonOffset);

        try {
          /* jQuery <1.7 does not support this */
          $addChildButtonClone.animate({
            "top": newElementOffset.top,
            "left": newElementOffset.left,
            "width": 150,
            "height": newElementHeight
          }, 800, "swing", function () {
            $addChildButtonClone.remove();
            $newElement.show();
          });
        } catch (e) {
          $addChildButtonClone.remove();
          $newElement.show();
        }

        /* Set new element icon */
        setElementTreeIcon(newElement);
      };
      var skip_rename = true;
      tree.jstree_cq("create", parent, position, newNodeConfig, callback, skip_rename);
      notifyListeners('change', {
        "what": "create",
        "element": newNodeConfig
      });
    }

    /**
     * Reads the editor for the selected node and updates the values.
     */
    function updateSelectedNode() {
      for (var i = 0; i < saveHandlers.length; i++) {
        saveHandlers[i].call(null);
      }
      return true;
    }

    /**
     * Clones the selected node
     */
    function cloneSelectedNode() {
      var $selectedNode = tree.find('a.jstree_cq-clicked').closest('li');
      var $clone = $selectedNode.clone(true);

      /**
       * update clone and its children
       */

      $clone.attr('id', $clone.attr('id') + '_' + (new Date().getTime())); //give it unique id
      jQuery('a', $clone).removeClass('jstree_cq-clicked');

      /* create separate clone of jstree_cq data, to lose references to original */
      $clone.data().jstree_cq = jQuery.extend(true, {}, $selectedNode.data().jstree_cq);

      $clone.find('li').each(function () {
        var $child = jQuery(this);

        $child.attr('id', $child.attr('id') + '_' + (new Date().getTime())); //give it unique id

        /* create separate clone of jstree_cq data, to lose references to original */
        $child.data().jstree_cq = jQuery.extend(true, {}, $child.data().jstree_cq);
      });

      /**
       * append clone to parent of selected node
       */
      $selectedNode.parent().append($clone);
    }

    /**
     * Removes the currently selected node from the tree.
     */
    function removeSelectedNode() {
      tree.jstree_cq("remove");
      emptyEditor();
      notifyListeners('change', {
        "what": "remove"
      });
    }

    /**
     * Returns the given string, with the first character in upper case.
     *
     * @param str
     *   The string to uppercase the first character of.
     */
    function _ucfirst(str) {
      if (!str) {
        return null;
      }
      var f = str.charAt(0).toUpperCase();
      return f + str.substr(1);
    }

    /**
     * Notify all listeners of type "type" that an event occurred, with the
     * given data.
     *
     * @param type
     *   The type of event.
     * @param data
     *   The data of the event.
     */
    function notifyListeners(type, data) {
      var i, value, returnValue;
      type = type.toLowerCase();
      if (listeners[type]) {
        for (i = 0; i < listeners[type].length; i++) {
          value = listeners[type][i].call(null, data);
          returnValue = returnValue === false ? false : value;
        }
      }

      return returnValue;
    }

    /**
     * Adds feedback to a attribute editor form element
     *
     * @param formElement
     *  A jQueried form DOM element
     * @param feedbackConfigArray
     *  Its feedback config object
     */
    function handleAttributeFeedback(formElement, feedbackConfigArray) {
      var feedbackCount = feedbackConfigArray.length;
      var i;
      var formElementValue = formElement.val();
      var feedbackObject = {};
      var feedbackConfig;
      var match;
      var stopFound = false;
      /* add new feedback */
      for (i = 0; i < feedbackCount; i++) {
        feedbackConfig = feedbackConfigArray[i];
        match = new RegExp(feedbackConfig.match);
        if (!stopFound && match.test(formElementValue) === true) {
          /* we have a match, and are allowed to process it.
           * Now find out what to do with it
           **/
          feedbackObject.text = feedbackConfig.text;
          if (feedbackConfig.correct !== undefined) {
            if (feedbackConfig.correct === 1) {
              /* style feedback text */
              feedbackObject.type = 'correct';
              /* enable form submitting */
              jQuery('#selectedNodeUpdateButton').removeAttr('disabled');
            }
            else {
              /* style feedback text */
              feedbackObject.type = 'error';
              if (feedbackObject.fatal === 1) {
                /* prevent form from submitting */
                jQuery('#selectedNodeUpdateButton').attr('disabled', 'disabled');
              }
            }

            /* show the feedback text */
            updateFormElementFeedback(formElement, feedbackConfig.match, feedbackObject);
          }

          if (feedbackConfig.stop === 1) {
            stopFound = true;
          }
        }
        else {
          /* remove feedback */
          updateFormElementFeedback(formElement, feedbackConfig.match, undefined);
        }
      }
    }

    /**
     * Private function to handle the search option.
     *
     * @param searchArguments
     *   Array of the arguments to the search option.
     *
     * @return
     *   The result of the search.
     */
    function _search(searchArguments) {
      var searchString = searchArguments[1].toString();
      var config = searchArguments[2] === undefined ? {} : searchArguments[2];
      var cssSelectorArray = [];
      var returnObject;
      var returnArray = [];
      var parent = config.parent || tree;
      var includeParent = config.includeParent || false; //parent can also be found

      var searchStringArray = searchString.split('/');
      for (var i = 0; i < searchStringArray.length; i++) {
        cssSelectorArray.push('li[rel=' + searchStringArray[i] + ']');
      }

      if (includeParent) {
        if (parent.attr('rel') !== searchStringArray[0]) {
          return jQuery();
        }
        else if (searchStringArray.length === 1) {
          /* search string is only one node deep, return the parent */
          return jQuery(parent);
        }
        else {
          /* remove first element from css selector array, which is the parent */
          cssSelectorArray.shift();
        }
      }

      returnObject = parent.find(cssSelectorArray.join(' > ul > '));
      returnObject.each(function () { //put return object items in 'normal' array
        returnArray.push(jQuery(this));
      });
      return returnArray;
    }

    /**
     * Private function that finds the closes parent of a node, that has a
     * certain type.
     *
     * @param searchArguments
     *   Array of the arguments to the search option.
     *
     * @return
     *   The result of the search.
     */
    function _closest(searchArguments) {
      var node = jQuery(searchArguments[1]);
      var ancestorType = searchArguments[2].toString();
      var closest = node.closest('li[rel=' + ancestorType + ']');
      return closest;
    }

    /**
     * Returns the config object
     *
     * @param nodeReference
     *  (Optional) The node type or the tree LI-node to return the config for
     * @param contextElement
     */
    function getConfig(nodeReference, contextElement) {
      var numberOfConditionSets;
      var conditionSet;
      var nodeType;
      var returnConfig;

      var i;
      if (nodeReference) {
        if (typeof nodeReference === 'string') {
          nodeType = nodeReference;
        }
        else {
          nodeType = jQuery(nodeReference).data().jstree_cq.type;
          if (!contextElement) {
            contextElement = nodeReference;
          }
        }

        returnConfig = {"types": {}};
        returnConfig.types[nodeType] = _config.types[nodeType];
      }
      else {
        returnConfig = _config;
      }

      /* is there a xml specific configuration? */
      if (contextElement && jQuery.isArray(_config.xml_specific_config)) {
        numberOfConditionSets = _config.xml_specific_config.length;
        for (i = 0; i < numberOfConditionSets; i++) {
          conditionSet = _config.xml_specific_config[i].conditions;
          if (matchConfigConditionSet(conditionSet, contextElement)) {
            /* yes, alter config */
            returnConfig = mergeObjects({}, returnConfig);
            returnConfig = mergeObjects(returnConfig, _config.xml_specific_config[i].config_changes);
          }
        }
      }

      /* return full config or only for nodeType */
      if (nodeType && returnConfig.types) {
        return returnConfig.types[nodeType];
      }
      else {
        return returnConfig;
      }
    }

    /**
     * Returns the current icon URL from a tree element.
     *
     * @param {object} element
     *
     * @returns {string}
     */
    function getElementIconURL(element) {
      var $iconElem = jQuery(element).find('.jstree_cq-icon').eq(1);
      return $iconElem.css('background-image').replace('url(', '').replace(')', '').replace(/\"/gi, '');
    }

    /**
     * Sets the current icon URL from a tree element.
     *
     * @param {object} element
     * @param {string} url
     */
    function setElementIconURL(element, url, updateFormIcon) {
      var $iconElem = jQuery(element).find('.jstree_cq-icon').eq(1);
      $iconElem.css('background-image', 'url(' + url + ')');
      if (updateFormIcon) {
        jQuery('.xmlJsonEditor_form_elements .xmlJsonEditor_icon').attr('src', url);
      }

    }

    /**
     * Sets the icon of an element.
     *
     * @param {obj} element
     *   The element.
     *
     * @param {obj} elementConfig
     *   Optional: the config of the element.
     *
     * @param {boolean} updateFormIcon
     *   If true, the form icon is updated as well. Default: false.
     */
    function setElementTreeIcon(element, elementConfig, updateFormIcon) {
      elementConfig = (typeof elementConfig === 'object') ? elementConfig : getConfig(element);
      updateFormIcon = (typeof updateFormIcon === 'boolean') ? updateFormIcon : false;
      var basePath = getConfig().basePath + '/assets/';
      var domIconUrl = getElementIconURL(element);
      var domIconPath = domIconUrl.substr(domIconUrl.indexOf(basePath));
      var configIconPath;
      /* Get default icon */
      if (elementConfig.icon && elementConfig.icon.image) {
        configIconPath = (elementConfig.icon.image.indexOf(basePath) === -1 ? basePath : '') + elementConfig.icon.image;
      }
      else {
        return; // No default icon specified for this element.
      }

      /* Set the icon */
      if (configIconPath !== domIconPath) {
        setElementIconURL(element, configIconPath, updateFormIcon);
      }
    }


    /**
     * Matches a config condition set
     *
     * @param conditionSet
     *
     * @param contextElement
     *
     * @param _type
     *   Private
     *
     * @return boolean
     */
    function matchConfigConditionSet(conditionSet, contextElement, _type) {
      var configCondition;
      var items;
      var j;
      var objectKeys;
      var sharedParent;
      var returnFlag;
      if (!jQuery.isArray(conditionSet)) {
        objectKeys = getObjectKeys(conditionSet);
        /* check whether logical operator */
        if (objectKeys.length === 1) {
          /* and/or */
          _type = objectKeys[0];
          if (matchConfigConditionSet(conditionSet[_type], contextElement, _type) === true) {
            return true;
          }
        }
        else {
          /* find a node */
          if (conditionSet.node || conditionSet.family_node) {
            /* find the node */
            if (conditionSet.node) {
              items = tree.xmlTreeEditor('search', conditionSet.node);
            }
            else {
              sharedParent = tree.xmlTreeEditor('closest', contextElement, conditionSet.family_node.split('/')[0]);
              items = tree.xmlTreeEditor('search', conditionSet.family_node, {
                "parent": sharedParent,
                "includeParent": true
              });
            }

            returnFlag = false;
            items.each(function () {
              var item = jQuery(this);
              var itemData = item.data().jstree_cq;
              if (itemData && itemData.attributes && conditionSet.attribute) {
                var itemAttributeValue = itemData.attributes[conditionSet.attribute];
                if (typeof itemAttributeValue !== 'undefined') {
                  if (new RegExp(conditionSet.attributeValue, "i").test(itemAttributeValue.toString()) === true) {
                    returnFlag = true;
                    return;
                  }
                }
                else if (conditionSet.attributeValue === null) { // Special case where icon should be shown if no attribute value
                  returnFlag = true;
                  return;
                }
              }
            });
          }

          return returnFlag;
        }
      }
      else {
        /* array with conditions */
        if (_type === "or") {
          for (j = 0; j < conditionSet.length; j++) {
            configCondition = conditionSet[j];
            if (matchConfigConditionSet(configCondition, contextElement, _type) === true) {
              return true;
            }
          }
        }
        else if (_type === "and") {
          for (j = 0; j < conditionSet.length; j++) {
            configCondition = conditionSet[j];
            if (matchConfigConditionSet(configCondition, contextElement, _type) === false) {
              return false;
            }
          }
          return true;
        }
      }

      return false;
    }

    /**
     * Makes a recursive copy of an object.
     *
     * @param object
     *   The object to copy.
     *
     * @return
     *   A copy of the passed object.
     */
    function copyObject(object) {
      return JSON.parse(JSON.stringify(object));
    }
    /**
     * Recursively merges an object into a target object.
     * Object children are merged.
     * Array children overwrite the original.
     * Non-object children overwrite the original.
     * The target is returned.
     *
     * @param target
     *   The object to merge the second object into.
     * @param object2
     *   The object to merge into the target object.
     *
     * @return
     *   The target object.
     */
    function mergeObjects(target, object2) {
      var i, key2;
      if (object2 !== undefined) {
        var type2 = typeof object2;
        switch (type2) {
          case "object":
            if (jQuery.isArray(object2)) {
              target = copyObject(object2);
            }
            else {
              if (typeof target !== "object") {
                target = copyObject(object2);
              }
              else {
                var object2keys = Object.keys(object2);
                var numberOfObject2keys = object2keys.length;
                for (var i = 0; i < numberOfObject2keys; i++) {
                  var key2 = object2keys[i];
                  if (object2[key2] === null) {
                    delete(target[key2]);
                  }
                  else {
                    target[key2] = mergeObjects(target[key2], object2[key2]);
                  }
                }
              }
            }
            break;
          case "array":
            target = copyObject(object2);
            break;
          default:
            target = object2;
            break;
        }
      }
      return target;
    }

    /**
     * Returns a select with all attached files (attached to this node that is)
     * @param {type} settings
     * @returns {object.fn.xmlTreeEditor.getFileAttachmentsImageSelect.$image_select}
     */
    function getFileAttachmentsImageSelect(settings) {
      settings = settings ? settings : {};

      var $image_select = jQuery('<select></select>');
      var filenameContainerSelector;

      var $fidInputs, fid;

      if (settings.showExternalImageOption) {
        $image_select.append('<option value="&lt;img src="add url here" title="add title here" /&gt;">External image</option>');
      }
      //add options
      var updateImageSelect = function () {
        var saved_value = $image_select.val();
        $image_select.html('');
        if (jQuery('.file').length > 0) {
          /* default Drupal file attachments */
          filenameContainerSelector = '.file';
        }
        else if (jQuery('.field-type-file label.media-filename').length > 0) {
          /* Media module */
          filenameContainerSelector = '.field-type-file label.media-filename';
          $fidInputs = jQuery('.field-type-file input.fid');
        }

        if (jQuery(filenameContainerSelector).length > 0) {
          $image_select.append('<option value="-1">Select image...</option>');
        }
        else {
          $image_select.append('<option value="-1">No images found. Add image under \'File Attachments\' below editor.</option>');
          $image_select.addClass('empty');
        }

        jQuery(filenameContainerSelector).each(function (i) {
          var filename = jQuery(this).text().trim();
          var optionValue = '';
          if (typeof $fidInputs === 'undefined') {
            /* default Drupal file attachments */
            optionValue = '<img src="[attachurl:' + filename + ']" title="' + filename + '" />';
          }
          else {
            /* Media module */
            fid = $fidInputs.eq(i) ? $fidInputs.eq(i).val() : -1;
            optionValue = '[[{"type":"media","view_mode":"default","fid":"' + fid + '","attributes":{"title":"' + filename + '","alt":"","class":"media-image","style":"width: auto; height: auto;","typeof":"foaf:Image"}}]]';
          }

          if ($image_select.children('option[value="' + filename + '"]').length === 0) {
            var $option = jQuery('<option value="' + filename + '" data-fid="' + fid + '">' + filename + '</option>');
            $option.data('fileplaceholder', optionValue);
            $option.data('fid', fid);
            $image_select.append($option);
          }

        });

        // Restore saved value.
        $image_select.val(saved_value);
      };
      updateImageSelect();

      $image_select.on('mouseenter', updateImageSelect);

      return $image_select;
    }

    /**
     * Returns al the keys of an object, sorted in an array. Non-recursive.
     * @param obj
     *  The object
     * @returns array
     */
    function getObjectKeys(obj) {
      var key;
      var returnArray = [];
      for (key in obj) {
        returnArray.push(key);
      }

      return returnArray.sort();
    }
  };
})(jQuery, Drupal);

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc