1/** 2 * Copyright © Magento, Inc. All rights reserved. 3 * See COPYING.txt for license details. 4 */ 5define([ 6 'jquery', 7 'underscore', 8 './loader' 9], function ($, _, loader) { 10 'use strict'; 11 12 var colonReg = /\\:/g, 13 renderedTemplatePromises = {}, 14 attributes = {}, 15 elements = {}, 16 globals = [], 17 renderer, 18 preset; 19 20 renderer = { 21 22 /** 23 * Loads template by provided path and 24 * than converts it's content to html. 25 * 26 * @param {String} tmplPath - Path to the template. 27 * @returns {jQueryPromise} 28 * @alias getRendered 29 */ 30 render: function (tmplPath) { 31 var cachedPromise = renderedTemplatePromises[tmplPath]; 32 33 if (!cachedPromise) { 34 cachedPromise = renderedTemplatePromises[tmplPath] = loader 35 .loadTemplate(tmplPath) 36 .then(renderer.parseTemplate); 37 } 38 39 return cachedPromise; 40 }, 41 42 /** 43 * @ignore 44 */ 45 getRendered: function (tmplPath) { 46 return renderer.render(tmplPath); 47 }, 48 49 /** 50 * Parses provided string as html content 51 * and returns an array of DOM elements. 52 * 53 * @param {String} html - String to be processed. 54 * @returns {Array} 55 */ 56 parseTemplate: function (html) { 57 var fragment = document.createDocumentFragment(); 58 59 $(fragment).append(html); 60 61 return renderer.normalize(fragment); 62 }, 63 64 /** 65 * Processes custom attributes and nodes of provided DOM element. 66 * 67 * @param {HTMLElement} content - Element to be processed. 68 * @returns {Array} An array of content's child nodes. 69 */ 70 normalize: function (content) { 71 globals.forEach(function (handler) { 72 handler(content); 73 }); 74 75 return _.toArray(content.childNodes); 76 }, 77 78 /** 79 * Adds new global content handler. 80 * 81 * @param {Function} handler - Function which will be invoked for 82 * an every content passed to 'normalize' method. 83 * @returns {Renderer} Chainable. 84 */ 85 addGlobal: function (handler) { 86 if (!_.contains(globals, handler)) { 87 globals.push(handler); 88 } 89 90 return this; 91 }, 92 93 /** 94 * Removes specified global content handler. 95 * 96 * @param {Function} handler - Handler to be removed. 97 * @returns {Renderer} Chainable. 98 */ 99 removeGlobal: function (handler) { 100 var index = globals.indexOf(handler); 101 102 if (~index) { 103 globals.splice(index, 1); 104 } 105 106 return this; 107 }, 108 109 /** 110 * Adds new custom attribute handler. 111 * 112 * @param {String} id - Attribute identifier. 113 * @param {(Object|Function)} [config={}] 114 * @returns {Renderer} Chainable. 115 */ 116 addAttribute: function (id, config) { 117 var data = { 118 name: id, 119 binding: id, 120 handler: renderer.handlers.attribute 121 }; 122 123 if (_.isFunction(config)) { 124 data.handler = config; 125 } else if (_.isObject(config)) { 126 _.extend(data, config); 127 } 128 129 data.id = id; 130 attributes[id] = data; 131 132 return this; 133 }, 134 135 /** 136 * Removes specified attribute handler. 137 * 138 * @param {String} id - Attribute identifier. 139 * @returns {Renderer} Chainable. 140 */ 141 removeAttribute: function (id) { 142 delete attributes[id]; 143 144 return this; 145 }, 146 147 /** 148 * Adds new custom node handler. 149 * 150 * @param {String}
150 id - Node identifier. 151 * @param {(Object|Function)} [config={}] 152 * @returns {Renderer} Chainable. 153 */ 154 addNode: function (id, config) { 155 var data = { 156 name: id, 157 binding: id, 158 handler: renderer.handlers.node 159 }; 160 161 if (_.isFunction(config)) { 162 data.handler = config; 163 } else if (_.isObject(config)) { 164 _.extend(data, config); 165 } 166 167 data.id = id; 168 elements[id] = data; 169 170 return this; 171 }, 172 173 /** 174 * Removes specified custom node handler. 175 * 176 * @param {String} id - Node identifier. 177 * @returns {Renderer} Chainable. 178 */ 179 removeNode: function (id) { 180 delete elements[id]; 181 182 return this; 183 }, 184 185 /** 186 * Checks if provided DOM element is a custom node. 187 * 188 * @param {HTMLElement} node - Node to be checked. 189 * @returns {Boolean} 190 */ 191 isCustomNode: function (node) { 192 return _.some(elements, function (elem) { 193 return elem.name.toUpperCase() === node.tagName; 194 }); 195 }, 196 197 /** 198 * Processes custom attributes of a content's child nodes. 199 * 200 * @param {HTMLElement} content - DOM element to be processed. 201 */ 202 processAttributes: function (content) { 203 var repeat; 204 205 repeat = _.some(attributes, function (attr) { 206 var attrName = attr.name, 207 nodes = content.querySelectorAll('[' + attrName + ']'), 208 handler = attr.handler; 209 210 return _.toArray(nodes).some(function (node) { 211 var data = node.getAttribute(attrName); 212 213 return handler(node, data, attr) === true; 214 }); 215 }); 216 217 if (repeat) { 218 renderer.processAttributes(content); 219 } 220 }, 221 222 /** 223 * Processes custom nodes of a provided content. 224 * 225 * @param {HTMLElement} content - DOM element to be processed. 226 */ 227 processNodes: function (content) { 228 var repeat; 229 230 repeat = _.some(elements, function (element) { 231 var nodes = content.querySelectorAll(element.name), 232 handler = element.handler; 233 234 return _.toArray(nodes).some(function (node) { 235 var data = node.getAttribute('args'); 236 237 return handler(node, data, element) === true; 238 }); 239 }); 240 241 if (repeat) { 242 renderer.processNodes(content); 243 } 244 }, 245 246 /** 247 * Wraps provided string in curly braces if it's necessary. 248 * 249 * @param {String} args - String to be wrapped. 250 * @returns {String} Wrapped string. 251 */ 252 wrapArgs: function (args) { 253 if (~args.indexOf('\\:')) { 254 args = args.replace(colonReg, ':'); 255 } else if (~args.indexOf(':') && !~args.indexOf('}')) { 256 args = '{' + args + '}'; 257 } 258 259 return args; 260 }, 261 262 /** 263 * Wraps child nodes of provided DOM element 264 * with knockout's comment tag. 265 * 266 * @param {HTMLElement} node - Node whose children should be wrapped. 267 * @param {String} binding - Name of the binding for the opener comment tag. 268 * @param {String} data - Data associated with a binding. 269 * 270 * @example 271 * <div id="example"><span/></div> 272 * wrapChildren(document.getElementById('example'), 'foreach', 'data'); 273 * => 274 * <div id="example"> 275 * <!-- ko foreach: data --> 276 * <span></span> 277 * <!-- /ko --> 278 * </div> 279 */ 280 wrapChildren: function (node, binding, data) { 281 var tag = this.createComment(binding, data), 282 $node = $(node); 283 284 $node.prepend(tag.open); 285 $node.append(tag.close); 286 }, 287 288 /** 289 * Wraps specified node with knockout's comment tag. 290 * 291 * @param {HTMLElement} node - Node to be wrapped. 292 * @param {String} binding - Name of the binding for the opener comment tag. 293 * @param {String} data - Data associated with a binding. 294 * 295 * @example 296 * <div id="example"></div> 297 * wrapNode(document.getElementById('example'), 'foreach', 'data'); 298 * => 299 * <!-- ko foreach: data --> 300 * <div id="example"></div> 301 * <!-- /ko --> 302 */ 303 wrapNode: function (node, binding, data) { 304 var tag = this.createComment(binding, data), 305 $node = $(node); 306 307 $node.before(tag.open); 308 $node.after(tag.close); 309 }, 310 311 /** 312 * Creates knockouts' comment tag for the provided binding. 313 * 314 * @param {String} binding - Name of the binding. 315 * @param {String} data - Data associated with a binding. 316 * @returns {Object} Object with an open and close comment elements. 317 */ 318 createComment: function (binding, data) { 319 return { 320 open: document.createComment(' ko ' + binding + ': ' + data + ' '), 321 close: document.createComment(' /ko ') 322 }; 323 } 324 }; 325 326 renderer.handlers = { 327 328 /** 329 * Basic node handler. Replaces custom nodes 330 * with a corresponding knockout's comment tag. 331 * 332 * @param {HTMLElement} node - Node to be processed. 333 * @param {String} data 334 * @param {Object} element 335 * @returns {Boolean} True 336 * 337 * @example Sample syntaxes conversions. 338 * <with args="model">
339 * <span/> 340 * </with> 341 * => 342 * <!-- ko with: model--> 343 * <span/> 344 * <!-- /ko --> 345 */ 346 node: function (node, data, element) { 347 data = renderer.wrapArgs(data); 348 349 renderer.wrapNode(node, element.binding, data); 350 $(node).replaceWith(node.childNodes); 351 352 return true; 353 }, 354 355 /** 356 * Base attribute handler. Replaces custom attributes with 357 * a corresponding knockouts' data binding. 358 * 359 * @param {HTMLElement} node - Node to be processed. 360 * @param {String} data - Data associated with a binding. 361 * @param {Object} attr - Attribute definition. 362 * 363 * @example Sample syntaxes conversions. 364 * <div text="label"></div> 365 * => 366 * <div data-bind="text: label"></div> 367 */ 368 attribute: function (node, data, attr) { 369 data = renderer.wrapArgs(data); 370 371 renderer.bindings.add(node, attr.binding, data); 372 node.removeAttribute(attr.name); 373 }, 374 375 /** 376 * Wraps provided node with a knockouts' comment tag. 377 * 378 * @param {HTMLElement} node - Node that will be wrapped. 379 * @param {String} data - Data associated with a binding. 380 * @param {Object} attr - Attribute definition. 381 * 382 * @example 383 * <div outereach="data" class="test"></div> 384 * => 385 * <!-- ko foreach: data --> 386 * <div class="test"></div> 387 * <!-- /ko --> 388 */ 389 wrapAttribute: function (node, data, attr) { 390 data = renderer.wrapArgs(data); 391 392 renderer.wrapNode(node, attr.binding, data); 393 node.removeAttribute(attr.name); 394 } 395 }; 396 397 renderer.bindings = { 398 399 /** 400 * Appends binding string to the current 401 * 'data-bind' attribute of provided node. 402 * 403 * @param {HTMLElement} node - DOM element whose 'data-bind' attribute will be extended. 404 * @param {String} name - Name of a binding. 405 * @param {String} data - Data associated with the binding. 406 */ 407 add: function (node, name, data) { 408 var bindings = this.get(node); 409 410 if (bindings) { 411 bindings += ', '; 412 } 413 414 bindings += name; 415 416 if (data) { 417 bindings += ': ' + data; 418 } 419 420 this.set(node, bindings); 421 }, 422 423 /** 424 * Extracts value of a 'data-bind' attribute from provided node. 425 * 426 * @param {HTMLElement} node - Node whose attribute to be extracted. 427 * @returns {String} 428 */ 429 get: function (node) { 430 return node.getAttribute('data-bind') || ''; 431 }, 432 433 /** 434 * Sets 'data-bind' attribute of the specified node 435 * to the provided value. 436 * 437 * @param {HTMLElement} node - Node whose attribute will be altered. 438 * @param {String} bindings - New value of 'data-bind' attribute. 439 */ 440 set: function (node, bindings) { 441 node.setAttribute('data-bind', bindings); 442 } 443 }; 444 445 renderer 446 .addGlobal(renderer.processAttributes) 447 .addGlobal(renderer.processNodes); 448 449 /** 450 * Collection of default binding conversions. 451 */ 452 preset = { 453 nodes: _.object([ 454 'if', 455 'text', 456 'with', 457 'scope', 458 'ifnot', 459 'foreach', 460 'component' 461 ], Array.prototype), 462 attributes: _.object([ 463 'css', 464 'attr', 465 'html', 466 'with', 467 'text', 468 'click', 469 'event', 470 'submit', 471 'enable', 472 'disable', 473 'options', 474 'visible', 475 'template', 476 'hasFocus', 477 'textInput', 478 'component', 479 'uniqueName', 480 'optionsText', 481 'optionsValue', 482 'checkedValue', 483 'selectedOptions' 484 ], Array.prototype) 485 }; 486 487 _.extend(preset.attributes, { 488 if: renderer.handlers.wrapAttribute, 489 ifnot: renderer.handlers.wrapAttribute, 490 innerif: { 491 binding: 'if' 492 }, 493 innerifnot: { 494 binding: 'ifnot' 495 }, 496 outereach: { 497 binding: 'foreach', 498 handler: renderer.handlers.wrapAttribute 499 }, 500 foreach: { 501 name: 'each' 502 }, 503 value: { 504 name: 'ko-value' 505 }, 506 style: { 507 name: 'ko-style' 508 }, 509 checked: { 510 name: 'ko-checked' 511 }, 512 disabled: { 513 name: 'ko-disabled', 514 binding: 'disable' 515 }, 516 focused: { 517 name: 'ko-focused', 518 binding: 'hasFocus' 519 }, 520 521 /** 522 * Custom 'render' attribute handler function. Wraps child elements 523 * of a node with knockout's 'ko template:' comment tag. 524 * 525 * @param {HTMLElement} node - Element to be processed. 526 * @param {String} data - Data specified in 'render' attribute of a node. 527 */ 528 render: function (node, data) { 529 data = data || 'getTemplate()'; 530 data = renderer.wrapArgs(data); 531 532 renderer.wrapChildren(node, 'template', data); 533 node.removeAttribute('render'); 534 } 535 }); 536 537 _.extend(preset.nodes, { 538 foreach: { 539 name: 'each' 540 }, 541 542 /** 543 * Custom 'render' node handler function. 544 * Replaces node with knockout's 'ko template:' comment tag. 545 * 546 * @param {HTMLElement} node - Element to be processed. 547 * @param {String} data - Data specified in 'args' attribute of a node. 548 */ 549 render: function (node, data) { 550 data = data || 'getTemplate()'; 551 data = renderer.wrapArgs(data); 552 553 renderer.wrapNode(node, 'template', data); 554 $(node).replaceWith(node.childNodes); 555 } 556 }); 557 558 _.each(preset.attributes, function (data, id) { 559 renderer.addAttribute(id, data); 560 }); 561 562 _.each(preset.nodes, function (data, id) { 563 renderer.addNode(id, data); 564 }); 565 566 return renderer; 567});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.