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(/ /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. "<p>...</p>" 2) Fix unsupported HTML entities, e.g. "&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> Add: </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> Table: </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="<img src="add url here" title="add title here" />">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);
