define([
'module',
'core/js/api/utils/StringUtils',
'pages.dt/js/ViewFactory',
'pages.dt/js/api/ViewGeneratorRegistry',
'pages.dt/js/api/ViewGeneratorSession',
'translations/js/api/Translatable'
], function(
module,
StringUtils,
ViewFactory,
viewGeneratorRegistry,
viewGeneratorSession,
Translatable
) {
'use strict';
/**
* Support class for view generators.
* <p>Provides number of helper methods useful for view generators building
* view DOMs such as applying view properties and binding to HTML templates
* or building child view DOMs.</p>
*
* @AbcsExtension stable
* @exports pages.dt/js/api/ViewGeneratorSupport
* @constructor
* @private
* @see {@link components.dt/js/spi/generators/Generator Generator}
* @version 16.3.5
*/
var ViewGeneratorSupport = function() {
AbcsLib.throwStaticClassError();
};
ViewGeneratorSupport._LOGGER = Logger.get(module.id);
ViewGeneratorSupport._CLONIG_VIEW_FACTORY = new ViewFactory();
ViewGeneratorSupport._FAKE_PAGE = {
getViewFactory: function() {
return new ViewFactory();
},
getViewModelDefinition: function() {
return {};
}
};
ViewGeneratorSupport._VIEWTYPE_CLASSSUFFIX_MAP = {
'comboBoxField': 'combobox',
'radioButtonsField': 'radiobuttons',
'attachment-component': 'attachment'
};
/**
* Applies {@link pages.dt/js/api/Binding Binding} object to the given string
* template.
*
* <p>
* This helper method for view generators allows you to define HTML for
* generated view DOMs in isolated template files and apply view bindings
* separately on the template content.
* </p>
*
* <p>It iterates over all key-value pairs in the Binding object and
* replaces all occurrences of the <code>key</code> with <code>value</code>
* in the given template content.</p>
*
* <p>See {@link pages.dt/js/api/ViewGeneratorSupport.applyMap ViewGeneratorSupport.applyMap}
* for the format of binding tokens you can use in templates.</p>
*
* @param {String} template HTML template containing <code>$something$</code> tokens.
* @param {pages.dt/js/api/Binding} [binding] Binding object to apply to the template.
* If <code>undefined</code> the original template will be immediately returned
* without any replacements.
* @returns {String} template updated HTML template with applied bindings
* @see {@link pages.dt/js/api/ViewGeneratorSupport.applyMap ViewGeneratorSupport.applyMap}
* for the format of binding tokens you can use in templates.
* @see {@link pages.dt/js/api/Binding Binding} for how and when to use Binding object.
* @example <caption>How to write template files with binding tokens</caption>
* <div>
* <p data-bind="text: $textValueObservable$"></p>
* </div>
*
* @example <caption>How to apply binding to a template</caption>
* define([
* 'pages.dt/js/api/ViewGeneratorSupport',
* 'text!myComponent/templates/componentTemplate.html' // see the above example for its content
* ], function (
* ViewGeneratorSupport,
* componentTemplate
* ) {
*
* 'use strict';
*
* var MyViewGenerator = function() {
* };
*
* MyViewGenerator.prototype.buildView = function (view, page) {
* var binding = view.getBinding();
* var template = ViewGeneratorSupport.applyBinding(componentTemplate, binding);
* var $element = $(template);
* return $element;
* };
*
* return MyViewGenerator;
* });
*/
ViewGeneratorSupport.applyBinding = function(template, binding) {
AbcsLib.checkDefined(template, 'template');
if (binding) {
return ViewGeneratorSupport.applyMap(template, binding.getDefinition());
}
return template;
};
/**
* Applies {@link pages.dt/js/api/Properties Properties} object to the given
* string template.
*
* <p>
* This helper method for view generators allows you to define HTML for
* generated view DOMs in isolated template files and apply view properties
* separately on the template content.
* </p>
*
* <p>It iterates over all key-value pairs in the Properties object and
* replaces all occurrences of the <code>key</code> with <code>value</code>
* in the given template content.</p>
*
* <p>See {@link pages.dt/js/api/ViewGeneratorSupport.applyMap ViewGeneratorSupport.applyMap}
* for the format of property tokens you can use in templates.</p>
*
* @AbcsExtension stable
* @version 16.3.5
* @param {String} template HTML template containing <code>$something$</code> tokens.
* @param {pages.dt/js/api/Properties} [properties] Properties object to apply to the template.
* If <code>undefined</code> the original template will be immediately returned
* without any replacements.
* @returns {String} template updated HTML template with applied properties
* @see {@link pages.dt/js/api/ViewGeneratorSupport.applyMap ViewGeneratorSupport.applyMap}
* for the format of property tokens you can use in templates.
* @see {@link pages.dt/js/api/Properties Properties} for how and when to use Properties object.
* @see {@link pages.dt/js/api/Binding Binding} for how and when to use Binding object.
* @example <caption>How to write template files with property tokens</caption>
* <div>
* <p>$textValue$</p>
* </div>
*
* @example <caption>How to apply properties to a template</caption>
* define([
* 'pages.dt/js/api/ViewGeneratorSupport',
* 'text!myComponent/templates/componentTemplate.html' // see the above example for its content
* ], function (
* ViewGeneratorSupport,
* componentTemplate
* ) {
*
* 'use strict';
*
* var MyViewGenerator = function() {
* };
*
* MyViewGenerator.prototype.buildView = function (view, page) {
* var properties = view.getProperties();
* var template = ViewGeneratorSupport.applyProperties(componentTemplate, properties);
* var $element = $(template);
* return $element;
* };
*
* return MyViewGenerator;
* });
*/
ViewGeneratorSupport.applyProperties = function(template, properties) {
AbcsLib.checkDefined(template, 'template');
if (properties) {
return ViewGeneratorSupport.applyMap(template, properties.getDefinition());
}
return template;
};
/**
* Applies object key-value pairs to the given string template.
*
* <p>
* This helper method for view generators allows you to define HTML for
* generated view DOMs in isolated template files containing placeholder
* tokens that are replaced with actual values at the time the generator
* is building the view's DOM.
* </p>
*
* <p>It iterates over all key-value pairs in the object and replaces all
* occurrences of all <code>"$key$"</code> tokens with the <code>value</code>
* in the given template content.</p>
*
* <p>Your template may contain placeholder tokens consisting of english
* <strong>alphanumeric characters or underscores (<code>_</code>) only</strong>,
* prefixed and suffixed with the dollar <code>$</code> character, for example:</p>
* <ul>
* <li>$textValue$</li>
* <li>$text_value$</li>
* <li>$textValue123$</li>
* </ul>
*
* <p>Tokens may be suffixed with a <code>/</code> directive specifying the
* type of value conversion that will take place before replacing in the
* template. This way you can easily tell Abcs to e.g. escape all special
* characters to prevent from a text be interpreted as HTML.</p>
* <dl>
* <dt><code>/html</code></dt>
* <dd>Replaces all occurrences of HTML special characters (<code><, >, &, "</code>)
* with HTML entities (&lt;, &gt;, &amp;, &quot;). That allows
* you to prevent from interpretting values as HTML or place tokens into attributes
* inside HTML tags. See the example below.</dd>
* </dl>
*
* @AbcsExtension stable
* @version 16.3.5
* @param {String} template HTML template containing <code>$something$</code> tokens.
* @param {Object} map map with key-value pairs to apply onto the template.
* @returns {String} template updated HTML template with applied replacements
* @example <caption>How to use applyMap</caption>
* var htmlTemplate = '<p>$textValue$</p><p>$text_value123$</p>';
* var tokenMap = {
* textValue: 'VALUE 1',
* text_value123: 'VALUE 2'
* };
* htmlTemplate = ViewGeneratorSupport.applyMap(htmlTemplate, tokenMap);
* // htmlTemplate will contain:
* // <p>VALUE 1</p><p>VALUE 2</p>
*
* @example <caption>How to use conversions</caption>
* // without using conversion
* var htmlTemplate = '<p data-custom="$attributeValue$">$textValue$</p>';
* var tokenMap = {
* textValue: '<p>TEXT</p>',
* attributeValue: 'attribute "VALUE"'
* };
* htmlTemplate = ViewGeneratorSupport.applyMap(htmlTemplate, tokenMap);
* // htmlTemplate will contain:
* // <p data-custom="attribute "VALUE""><p>TEXT</p></p>
* // which is obviously an invalid HTML
*
* // now with conversions
* htmlTemplate = '<p data-custom="$attributeValue/html$">$textValue/html$</p>';
* htmlTemplate = ViewGeneratorSupport.applyMap(htmlTemplate, tokenMap);
* // htmlTemplate will contain:
* // <p data-custom="attribute "VALUE""><p>TEXT</p></p>
*
*/
ViewGeneratorSupport.applyMap = function(template, map) {
AbcsLib.checkDefined(template, 'template');
AbcsLib.checkDefined(map, 'map');
// Replace property key name with optional formatting function.
// Supports strings like $key/fn$, e.g. $label/html$.
var varRegExp = /\$(\w+)(?:\/([^\$]+))?\$/g;
// Method replace takes a function as the last parameter here. The
// function is called for each matched string and returns custom
// replacement string. In this case, it takes three arguments, the
// first one is the whole match, the second one is the first
// parenthesized submatch string, i.e. variable name, and the third one
// is second second submatch string, i.e. escpaing mark. Thus, argument
// variable contains key to replace, and escapingMark contains optional
// formatting flag. E.g. for key "term" and template variable
// $term/html$, the value of variable will be "term" and value of
// escapingMark will be "html".
return template.replace(varRegExp, function(match, variable, escapingMark) {
if (!(variable in map)) {
return match; // if key is not in passed map, do not replace
}
var origReplacingValue = map[variable];
var replacingValue = origReplacingValue;
// empty array is coverted to '' by toString
if (replacingValue instanceof Array) {
if (!replacingValue.length) {
replacingValue = '[]';
}
}
var val;
switch (escapingMark) {
case 'html':
val = StringUtils.escapeHtml(replacingValue);
break;
case 'stringJS':
val = StringUtils.escapeStringJS(replacingValue);
break;
case 'objectJS':
val = StringUtils.escapeObjectJS(origReplacingValue);
break;
case 'i18n':
val = StringUtils.escapeTranslatable(origReplacingValue);
break;
case 'help':
val = StringUtils.escapeHelp(origReplacingValue);
break;
case 'helpURL':
val = StringUtils.escapeHelpURL(replacingValue);
break;
default:
val = replacingValue;
break;
}
return val;
});
};
/**
* Builds the DOM tree for the given view.
* <p>Finds proper generator bound with the given view and runs it. This
* method is useful when your view contains child views and you intend to
* build them and append their DOM trees into the top view's DOM.</p>
*
* @AbcsExtension stable
* @version 16.3.5
* @param {pages.dt/js/api/View} view view to build DOM for.
* @param {pages.dt/js/api/Page} page page containing the view.
* @example
* <caption>Building DOM elements for child views</caption>
* MyViewGenerator.prototype.buildView = function (view, page) {
* // build view DOM element
* var $element = $('<div>');
*
* // iterate all children and build their DOM elements
* view.getChildren().forEach(function (childView) {
* // build child view element
* var $childElement = ViewGeneratorSupport.buildElement(childView, page);
* $element.append($childElement);
* });
*
* return $element;
* };
*/
ViewGeneratorSupport.buildElement = function (view, page) {
AbcsLib.checkDefined(view, 'view');
AbcsLib.checkDefined(page, 'page');
var $element;
var mode = viewGeneratorSession.getGeneratorMode();
try {
if (page === ViewGeneratorSupport._FAKE_PAGE) {
page = undefined;
}
$element = ViewGeneratorSupport._prepareElement(view, page);
} catch (exception) {
var msg = 'Error building DOM element for view ' + JSON.stringify(view.getDefinition(), null, 4);
ViewGeneratorSupport._LOGGER.exception(exception, msg);
$element = ViewGeneratorSupport._prepareBrokenElement(view, exception);
}
ViewGeneratorSupport._modifyElementForDev($element, view, mode);
return $element;
};
/**
* Starts a new generator session with the passed generator mode and
* builds a view DOM element within the session.
*
* @param {View} view
* @param {ViewGeneratorModes.Mode} mode
* @param {Page} page
* @returns {jQuery}
*/
ViewGeneratorSupport.buildElementInMode = function (view, mode, page) {
var $element;
viewGeneratorSession.runSession(mode, function() {
page = page || ViewGeneratorSupport._FAKE_PAGE;
var clonedView = ViewGeneratorSupport._cloneView(view, page);
$element = ViewGeneratorSupport.buildElement(clonedView, page);
});
return $element;
};
ViewGeneratorSupport._prepareBrokenElement = function (view) {
var $element = $('<div></div>');
var viewId = view.getId();
$element.attr({
'data-view-id': viewId,
'class': 'broken-view'
});
$element.append('Invalid Component \'' + viewId + '\'');
viewGeneratorSession.getProblems(viewId).forEach(function (problemDescription) {
$element.append('<br>' + problemDescription);
});
return $element;
};
ViewGeneratorSupport._cloneView = function(view, page) {
//clone the view itself
var clonedView = ViewGeneratorSupport._CLONIG_VIEW_FACTORY.createViewFromDefinition(view.getDefinition(), page.getViewModelDefinition());
//owner - undefined after cloning by definition but mandatory for getting enclosing archetype
clonedView.setOwner(view.getOwner());
return clonedView;
};
ViewGeneratorSupport._modifyElementForDev = function ($element, view, mode) {
if (mode && mode.isDesigner()) {
var parent = view.getParent();
$element.attr({
'data-parent-view-id': parent ? parent.getId() : '',
'data-view-type': view.getType(),
'data-is-wrappable': view.isWrappable(),
'data-is-droppable': view.isDroppable(),
'data-is-vertically-resizable': view.isVerticallyResizable(),
'data-is-horizontally-resizable': view.isHorizontallyResizable(),
'data-is-rerender-parent': view.isRerenderParent()
});
$element.addClass('abcs-component');
}
};
ViewGeneratorSupport._prepareElement = function (view, page) {
var viewGenerator = viewGeneratorRegistry.getViewGenerator(view);
if (!viewGenerator) {
throw new Error('can\'t find view generator for type ' + view.getType() + ', id: ' + view.getId());
}
var $element = viewGenerator.buildView(view, page);
if (!$element) {
throw new Error('The ViewGenerator \'' + view.getType() + '\' has not returned a DOM element!');
}
$element.attr({
'data-view-id': view.getId(),
'data-component-type': view.getType()
});
ViewGeneratorSupport._addComponentClass($element, view);
return $element;
};
ViewGeneratorSupport._addComponentClass = function ($element, view) {
var cls = ViewGeneratorSupport._userFriendlyClassSuffix(view.getType());
if (cls) {
$element.addClass('abcs-component-' + cls);
}
};
ViewGeneratorSupport._userFriendlyClassSuffix = function (viewType) {
if (!viewType) {
return undefined;
}
// Check if there is a custom value and if so, return it.
var custom = ViewGeneratorSupport._VIEWTYPE_CLASSSUFFIX_MAP[viewType];
if (custom) {
return custom;
}
// Convert to lower-case and remove non-alphanumeric characters.
var converted = viewType.toLowerCase().replace(/[^a-z0-9]/gi, '');
return converted;
};
/**
* Create and serialize an object to be used inside data-bind attribute. It
* can be used where JSON.stringify is not sufficient because it doesn't
* support function definitions.
*
* @param {function} fn - Function that return an object. It will be passed
* a single argument, called 'makeExpression' which you can use to create a
* literal expressions that will not be stringified. Function makeExpression
* gets two arguments, the first one is the actual value, and the second
* one, optional, is an object that can be applied with
* ViewGenerator.applyMap on the first argument.
*
* @param {boolean} suppressParentheses - True to suppress parentheses at
* the first level of the hierarchy. It is useful when putting the result
* string directly into "data-bind" HTML parameter.
*
* @returns {string}
*/
ViewGeneratorSupport.serializeObject = function (fn, suppressParentheses) {
var str = function (val) {
if (val === undefined) {
return 'undefined';
}
return JSON.stringify(val);
};
// Expression node.
var E = function (val, map) {
this.v = map ? ViewGeneratorSupport.applyMap(val, map) : val;
};
E.prototype.asExpression = function () {
return this.v;
};
// Expression generator.
var e = function (val, map) {
return new E(val, map);
};
var serialize, serializeObject, serializeArray, serializeTranslatable; //Define before use, hm.
serialize = function (value, suppressParens) {
if (value && value.constructor === E) {
return value.v;
} else if (AbcsLib.isInstanceof(value, Translatable)) {
return serializeTranslatable(value);
} else if (AbcsLib.isObject(value)) {
return serializeObject(value, suppressParens);
} else if (AbcsLib.isArray(value)) {
return serializeArray(value, suppressParens);
} else {
return str(value);
}
};
serializeObject = function (obj, suppressParens) {
var res = suppressParens ? '' : '{';
var first = true;
for (var name in obj) {
if (obj.hasOwnProperty(name)) {
var prop = obj[name];
var seg = JSON.stringify(name) + ':' + serialize(prop, false);
res = res + (first ? seg : ',' + seg);
first = false;
}
}
return res + (suppressParens ? '' : '}');
};
serializeArray = function (array, suppressParens) {
var res = suppressParens ? '' : '[';
array.forEach(function (val, index) {
if (index) {
res += ',';
}
res += serialize(val, false);
});
return res + (suppressParens ? '' : ']');
};
serializeTranslatable = function (translatable) {
return translatable.toTranslatableString();
};
var obj = fn(e);
return serialize(obj, suppressParentheses);
};
return ViewGeneratorSupport;
});