/** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ define([ 'jquery', 'underscore', './loader' ], function ($, _, loader) { 'use strict'; var colonReg = /\\:/g, renderedTemplatePromises = {}, attributes = {}, elements = {}, globals = [], renderer, preset; renderer = { /** * Loads template by provided path and * than converts it's content to html. * * @param {String} tmplPath - Path to the template. * @returns {jQueryPromise} * @alias getRendered */ render: function (tmplPath) { var cachedPromise = renderedTemplatePromises[tmplPath]; if (!cachedPromise) { cachedPromise = renderedTemplatePromises[tmplPath] = loader .loadTemplate(tmplPath) .then(renderer.parseTemplate); } return cachedPromise; }, /** * @ignore */ getRendered: function (tmplPath) { return renderer.render(tmplPath); }, /** * Parses provided string as html content * and returns an array of DOM elements. * * @param {String} html - String to be processed. * @returns {Array} */ parseTemplate: function (html) { var fragment = document.createDocumentFragment(); $(fragment).append(html); return renderer.normalize(fragment); }, /** * Processes custom attributes and nodes of provided DOM element. * * @param {HTMLElement} content - Element to be processed. * @returns {Array} An array of content's child nodes. */ normalize: function (content) { globals.forEach(function (handler) { handler(content); }); return _.toArray(content.childNodes); }, /** * Adds new global content handler. * * @param {Function} handler - Function which will be invoked for * an every content passed to 'normalize' method. * @returns {Renderer} Chainable. */ addGlobal: function (handler) { if (!_.contains(globals, handler)) { globals.push(handler); } return this; }, /** * Removes specified global content handler. * * @param {Function} handler - Handler to be removed. * @returns {Renderer} Chainable. */ removeGlobal: function (handler) { var index = globals.indexOf(handler); if (~index) { globals.splice(index, 1); } return this; }, /** * Adds new custom attribute handler. * * @param {String} id - Attribute identifier. * @param {(Object|Function)} [config={}] * @returns {Renderer} Chainable. */ addAttribute: function (id, config) { var data = { name: id, binding: id, handler: renderer.handlers.attribute }; if (_.isFunction(config)) { data.handler = config; } else if (_.isObject(config)) { _.extend(data, config); } data.id = id; attributes[id] = data; return this; }, /** * Removes specified attribute handler. * * @param {String} id - Attribute identifier. * @returns {Renderer} Chainable. */ removeAttribute: function (id) { delete attributes[id]; return this; }, /** * Adds new custom node handler. * * @param {String} id - Node identifier. * @param {(Object|Function)} [config={}] * @returns {Renderer} Chainable. */ addNode: function (id, config) { var data = { name: id, binding: id, handler: renderer.handlers.node }; if (_.isFunction(config)) { data.handler = config; } else if (_.isObject(config)) { _.extend(data, config); } data.id = id; elements[id] = data; return this; }, /** * Removes specified custom node handler. * * @param {String} id - Node identifier. * @returns {Renderer} Chainable. */ removeNode: function (id) { delete elements[id]; return this; }, /** * Checks if provided DOM element is a custom node. * * @param {HTMLElement} node - Node to be checked. * @returns {Boolean} */ isCustomNode: function (node) { return _.some(elements, function (elem) { return elem.name.toUpperCase() === node.tagName; }); }, /** * Processes custom attributes of a content's child nodes. * * @param {HTMLElement} content - DOM element to be processed. */ processAttributes: function (content) { var repeat; repeat = _.some(attributes, function (attr) { var attrName = attr.name, nodes = content.querySelectorAll('[' + attrName + ']'), handler = attr.handler; return _.toArray(nodes).some(function (node) { var data = node.getAttribute(attrName); return handler(node, data, attr) === true; }); }); if (repeat) { renderer.processAttributes(content); } }, /** * Processes custom nodes of a provided content. * * @param {HTMLElement} content - DOM element to be processed. */ processNodes: function (content) { var repeat; repeat = _.some(elements, function (element) { var nodes = content.querySelectorAll(element.name), handler = element.handler; return _.toArray(nodes).some(function (node) { var data = node.getAttribute('args'); return handler(node, data, element) === true; }); }); if (repeat) { renderer.processNodes(content); } }, /** * Wraps provided string in curly braces if it's necessary. * * @param {String} args - String to be wrapped. * @returns {String} Wrapped string. */ wrapArgs: function (args) { if (~args.indexOf('\\:')) { args = args.replace(colonReg, ':'); } else if (~args.indexOf(':') && !~args.indexOf('}')) { args = '{' + args + '}'; } return args; }, /** * Wraps child nodes of provided DOM element * with knockout's comment tag. * * @param {HTMLElement} node - Node whose children should be wrapped. * @param {String} binding - Name of the binding for the opener comment tag. * @param {String} data - Data associated with a binding. * * @example *
* wrapChildren(document.getElementById('example'), 'foreach', 'data'); * => *
* * * *
*/ wrapChildren: function (node, binding, data) { var tag = this.createComment(binding, data), $node = $(node); $node.prepend(tag.open); $node.append(tag.close); }, /** * Wraps specified node with knockout's comment tag. * * @param {HTMLElement} node - Node to be wrapped. * @param {String} binding - Name of the binding for the opener comment tag. * @param {String} data - Data associated with a binding. * * @example *
* wrapNode(document.getElementById('example'), 'foreach', 'data'); * => * *
* */ wrapNode: function (node, binding, data) { var tag = this.createComment(binding, data), $node = $(node); $node.before(tag.open); $node.after(tag.close); }, /** * Creates knockouts' comment tag for the provided binding. * * @param {String} binding - Name of the binding. * @param {String} data - Data associated with a binding. * @returns {Object} Object with an open and close comment elements. */ createComment: function (binding, data) { return { open: document.createComment(' ko ' + binding + ': ' + data + ' '), close: document.createComment(' /ko ') }; } }; renderer.handlers = { /** * Basic node handler. Replaces custom nodes * with a corresponding knockout's comment tag. * * @param {HTMLElement} node - Node to be processed. * @param {String} data * @param {Object} element * @returns {Boolean} True * * @example Sample syntaxes conversions. * * * * => * * * */ node: function (node, data, element) { data = renderer.wrapArgs(data); renderer.wrapNode(node, element.binding, data); $(node).replaceWith(node.childNodes); return true; }, /** * Base attribute handler. Replaces custom attributes with * a corresponding knockouts' data binding. * * @param {HTMLElement} node - Node to be processed. * @param {String} data - Data associated with a binding. * @param {Object} attr - Attribute definition. * * @example Sample syntaxes conversions. *
* => *
*/ attribute: function (node, data, attr) { data = renderer.wrapArgs(data); renderer.bindings.add(node, attr.binding, data); node.removeAttribute(attr.name); }, /** * Wraps provided node with a knockouts' comment tag. * * @param {HTMLElement} node - Node that will be wrapped. * @param {String} data - Data associated with a binding. * @param {Object} attr - Attribute definition. * * @example *
* => * *
* */ wrapAttribute: function (node, data, attr) { data = renderer.wrapArgs(data); renderer.wrapNode(node, attr.binding, data); node.removeAttribute(attr.name); } }; renderer.bindings = { /** * Appends binding string to the current * 'data-bind' attribute of provided node. * * @param {HTMLElement} node - DOM element whose 'data-bind' attribute will be extended. * @param {String} name - Name of a binding. * @param {String} data - Data associated with the binding. */ add: function (node, name, data) { var bindings = this.get(node); if (bindings) { bindings += ', '; } bindings += name; if (data) { bindings += ': ' + data; } this.set(node, bindings); }, /** * Extracts value of a 'data-bind' attribute from provided node. * * @param {HTMLElement} node - Node whose attribute to be extracted. * @returns {String} */ get: function (node) { return node.getAttribute('data-bind') || ''; }, /** * Sets 'data-bind' attribute of the specified node * to the provided value. * * @param {HTMLElement} node - Node whose attribute will be altered. * @param {String} bindings - New value of 'data-bind' attribute. */ set: function (node, bindings) { node.setAttribute('data-bind', bindings); } }; renderer .addGlobal(renderer.processAttributes) .addGlobal(renderer.processNodes); /** * Collection of default binding conversions. */ preset = { nodes: _.object([ 'if', 'text', 'with', 'scope', 'ifnot', 'foreach', 'component' ], Array.prototype), attributes: _.object([ 'css', 'attr', 'html', 'with', 'text', 'click', 'event', 'submit', 'enable', 'disable', 'options', 'visible', 'template', 'hasFocus', 'textInput', 'component', 'uniqueName', 'optionsText', 'optionsValue', 'checkedValue', 'selectedOptions' ], Array.prototype) }; _.extend(preset.attributes, { if: renderer.handlers.wrapAttribute, ifnot: renderer.handlers.wrapAttribute, innerif: { binding: 'if' }, innerifnot: { binding: 'ifnot' }, outereach: { binding: 'foreach', handler: renderer.handlers.wrapAttribute }, foreach: { name: 'each' }, value: { name: 'ko-value' }, style: { name: 'ko-style' }, checked: { name: 'ko-checked' }, disabled: { name: 'ko-disabled', binding: 'disable' }, focused: { name: 'ko-focused', binding: 'hasFocus' }, /** * Custom 'render' attribute handler function. Wraps child elements * of a node with knockout's 'ko template:' comment tag. * * @param {HTMLElement} node - Element to be processed. * @param {String} data - Data specified in 'render' attribute of a node. */ render: function (node, data) { data = data || 'getTemplate()'; data = renderer.wrapArgs(data); renderer.wrapChildren(node, 'template', data); node.removeAttribute('render'); } }); _.extend(preset.nodes, { foreach: { name: 'each' }, /** * Custom 'render' node handler function. * Replaces node with knockout's 'ko template:' comment tag. * * @param {HTMLElement} node - Element to be processed. * @param {String} data - Data specified in 'args' attribute of a node. */ render: function (node, data) { data = data || 'getTemplate()'; data = renderer.wrapArgs(data); renderer.wrapNode(node, 'template', data); $(node).replaceWith(node.childNodes); } }); _.each(preset.attributes, function (data, id) { renderer.addAttribute(id, data); }); _.each(preset.nodes, function (data, id) { renderer.addNode(id, data); }); return renderer; });