1 (function (GCN) { 2 3 'use strict'; 4 5 /** 6 * Only grows, never shrinks. 7 * 8 * @private 9 * @type {number} 10 */ 11 var uniqueIdCounter = 0; 12 13 /** 14 * Generates a unique id with an optional prefix. 15 * 16 * The returned value is only unique among other returned values, 17 * not globally. 18 * 19 * @public 20 * @param {string} 21 * Optional prefix for the id to be generated. 22 * @return {string} 23 * Never the same string more than once. 24 */ 25 function uniqueId(prefix) { 26 return (prefix || '') + (++uniqueIdCounter); 27 } 28 29 /** 30 * Escapes the given property name by prefixing dots with backslashes. 31 * 32 * @param {string} name The property name to escape. 33 * @return {string} Escaed string. 34 */ 35 function escapePropertyName(name) { 36 return name.replace(/\./g, '\\.'); 37 } 38 39 /** 40 * A regular expression to identify executable script tags. 41 * 42 * @type {RegExp} 43 */ 44 var rgxpScriptType = /\/(java|ecma)script/i; 45 46 47 /** 48 * A string to be used in regular expressions matching script tags. 49 * 50 * TODO: The string does not work correctly for complex cases and 51 * should be improved. 52 * 53 * @type {string} 54 */ 55 var SCRIPT_TAG = '<script(\\s[^>]*?)?>'; 56 57 /** 58 * This regular expression is used to find opening script tags. 59 * 60 * It uses the global flag to find several matching tags, for 61 * example to replace them. 62 * 63 * @type {RegExp} 64 */ 65 var rgxpScriptTags = new RegExp(SCRIPT_TAG, 'ig'); 66 67 /** 68 * This regular expression is used to find opening script tags. 69 * It does not use the global flag, and can be used to test if 70 * a string contains scripts at all. 71 * 72 * For testing strings, this expression should be used instead 73 * of rgxpScriptTags, because the latter keeps an index of found 74 * occurrences (due to its global flag), which causes the 75 * regular expression to often fail (in Firefox). 76 * 77 * @type {RegExp} 78 */ 79 var rgxpScriptTag = new RegExp(SCRIPT_TAG, 'i'); 80 81 82 83 /** 84 * A regular expression to find script types. 85 * 86 * @type {RegExp} 87 */ 88 var rgxpType = new RegExp( 89 ' type\\s*=\\s*' 90 + '[\\"\\\']' 91 + '([^\\"\\\']*[^\\s][^\\"\\\']*)' 92 + '[\\"\\\']', 93 'i' 94 ); 95 96 var rand = Math.random().toString().replace('.', ''); 97 98 /** 99 * Places sentinal strings in front of every executable script tag. 100 * 101 * @param {string} html HTML markup 102 * @return {string} HTML string with marked <script> tags. 103 */ 104 function markScriptTagLocations(html) { 105 var i = 0; 106 rgxpScriptTags.lastIndex = 0; 107 return html.replace(rgxpScriptTags, function (str, substr, offset) { 108 var type = substr && substr.match(rgxpType); 109 if (!type || rgxpScriptType.test(type)) { 110 return rand + (i++) + str; 111 } 112 return str; 113 }); 114 } 115 116 /** 117 * Masks a <script> tag's `type' attribute by replacing it with a random 118 * string. 119 * 120 * Masking script tags is done to prevent them from being handled specially 121 * by jQuery, which removes javascript/ecmascript tags when appending DOM 122 * elements into the document, but executes them. 123 * 124 * unmakeScriptType() reverses this. 125 * 126 * @param {jQuery.<HTMLElement>} $script jQuery unit set containing the 127 * <script> tag that is to have its 128 * type attribute masked. 129 */ 130 function maskScriptType($script) { 131 var type = $script.attr('type'); 132 $script.attr('type', rand).attr('data-origtype', type); 133 } 134 135 /** 136 * Restores a <script> tag's original type attribute value if it had been 137 * masked using maskScriptType(). 138 * 139 * Essentially the reverse of maskScriptType(). 140 * 141 * @param {jQuery.<HTMLElement>} $script jQuery unit set containing the 142 * <script> tag that is to have its 143 * type attribute unmasked. 144 */ 145 function unmaskScriptType($script) { 146 var orig = $script.attr('data-origtype'); 147 if (typeof orig === 'string') { 148 $script.attr('type', orig).removeAttr('data-origtype'); 149 } else { 150 $script.removeAttr('type'); 151 } 152 } 153 154 /** 155 * Replaces the type attribute of <script> tags with a value that will 156 * protect them from being specially handled by jQuery. 157 * 158 * @param {string} html Markup 159 * @return {string} Markup with <script> tags protected from jQuery. 160 */ 161 function protectScriptTags(html, $scripts) { 162 var i; 163 var type; 164 var $script; 165 for (i = 0; i < $scripts.length; i++) { 166 $script = $scripts.eq(i); 167 type = $script.attr('type'); 168 if (!type || rgxpScriptType.test(type)) { 169 maskScriptType($script); 170 html = html.replace(rand + i, $script[0].outerHTML); 171 unmaskScriptType($script); 172 } 173 } 174 return html; 175 } 176 177 /** 178 * Restores the type attribute for <script> tags that have been processed 179 * via protectScriptTags(). 180 * 181 * @param {jQuery.<HTMLElement>} $element Root HTMLElement in which to 182 * restore <script> tags. 183 */ 184 function restoreScriptTagTypes($element) { 185 var $scripts = $element.find('script[type="' + rand + '"]'); 186 var $script; 187 var i; 188 for (i = 0; i < $scripts.length; i++) { 189 $script = $scripts.eq(i); 190 $script.removeClass(rand + i); 191 unmaskScriptType($script); 192 } 193 } 194 195 /** 196 * Joins the innerHTML of multiple elements. 197 * 198 * @param {jQuery.<HTMLElement>} $elements 199 * @return {string} A concatenated string of the contents of the given set 200 * of elements. 201 */ 202 function joinContents($elements) { 203 var contents = ''; 204 $elements.each(function () { 205 contents += jQuery(this).html(); 206 }); 207 return contents; 208 } 209 210 /** 211 * Inserts the inner HTML of the given HTML markup while preserving 212 * <script> tags, and still allowing jQuery to execute them. 213 * 214 * @param {jQuery.<HTMLElement>} $element 215 * @param {string} html 216 */ 217 function insertInnerHTMLWithScriptTags($element, html) { 218 if (!rgxpScriptTag.test(html)) { 219 $element.html(jQuery(html).contents()); 220 return; 221 } 222 var marked = markScriptTagLocations(html); 223 var $scripts = jQuery(html).filter('script'); 224 var $html = jQuery(marked); 225 var contents = joinContents($html.filter(':not(script)')); 226 contents = protectScriptTags(contents, $scripts); 227 $element.html(contents); 228 229 // Trap script errors originating from rendered tags and log it on the 230 // console. 231 try { 232 $element.append($scripts); 233 } catch (ex) { 234 var _console = 'console'; // Because jslint is paranoid. 235 if (typeof window[_console] === 'function') { 236 window[_console].error(ex); 237 } 238 } 239 240 restoreScriptTagTypes($element); 241 } 242 243 /** 244 * Merge class names from one element into another. 245 * 246 * The merge result will be a unqiue set of space-seperated class names. 247 * 248 * @param {jQuery.<HTMLElement>} $first jQuery unit set containing the DOM 249 * whose class names are to be merged. 250 * @param {jQuery.<HTMLElement>} $second jQuery unit set containing the DOM 251 * whose class names are to be merged. 252 * @return {string} The merge result of the merge: a unqiue set of 253 * space-seperated class names. 254 */ 255 function mergeClassNames($first, $second) { 256 var first = ($first.attr('class') || '').split(' '); 257 var second = ($second.attr('class') || '').split(' '); 258 var names = first.concat(second).sort(); 259 var i; 260 for (i = 1; i < names.length; i++) { 261 if (names[i] === names[i - 1]) { 262 names.splice(i--, 1); 263 } 264 } 265 return names.join(' '); 266 } 267 268 /** 269 * Creates a map of attributes merged from their value in $from with their 270 * value in $to. 271 * 272 * Class names--unlike other attributes, which are simply copied from $from 273 * into $to--are treaded specially to produce a unique set of 274 * space-seperated class names. 275 * 276 * @param {jQuery.<HTMLElement>} $to jQuery unit set containing the DOM 277 * element which should receive the 278 * merged attributes. 279 * @param {jQuery.<HTMLElement>} $from jQuery unit set containing the DOM 280 * element whose attributes will be 281 * merged into the other. 282 * @param {object<string, string>} A associate array of attributes. 283 */ 284 function mergeAttributes($to, $from) { 285 var from = $from[0].attributes; 286 var to = $to[0].attributes; 287 var i; 288 var attr = {}; 289 for (i = 0; i < from.length; i++) { 290 attr[from[i].name] = ('class' === from[i].name) 291 ? mergeClassNames($to, $from) 292 : $from.attr(from[i].name); 293 } 294 return attr; 295 } 296 297 /** 298 * Renders the given HTML string onto (not into) an DOM element. 299 * Does nearly the equivelent of $.replaceWith() or changing the element's 300 * outerHTML. 301 * 302 * http://bugs.jquery.com/ticket/8142#comment:6 303 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 304 * 'All of jQuery's insertion methods use a domManip function internally to 305 * clean/process elements before and after they are inserted into the DOM. 306 * One of the things the domManip function does is pull out any script 307 * elements about to be inserted and run them through an "evalScript 308 * routine" rather than inject them with the rest of the DOM fragment. It 309 * inserts the scripts separately, evaluates them, and then removes them 310 * from the DOM. 311 * 312 * 'I believe that one of the reasons jQuery does this is to avoid 313 * "Permission Denied" errors that can occur in Internet Explorer when 314 * inserting scripts under certain circumstances. It also avoids repeatedly 315 * inserting/evaluating the same script (which could potentially cause 316 * problems) if it is within a containing element that you are inserting and 317 * then moving around the DOM.' 318 * 319 * @param {jQuery.<HTMLElement>} $element jQuery unit set containing the 320 * DOM element we wish to render the 321 * given html content onto. 322 * @param {string} html HTML content which will become give to the element. 323 */ 324 function renderOnto($element, html) { 325 insertInnerHTMLWithScriptTags($element, html); 326 var attr = mergeAttributes($element, jQuery(html)); 327 var name; 328 for (name in attr) { 329 if (attr.hasOwnProperty(name)) { 330 $element.attr(name, attr[name]); 331 } 332 } 333 } 334 335 /** 336 * Removes the given chainback instance from its cached location. 337 * 338 * @param {Chainback} chainback Instance to remove from cache. 339 */ 340 function decache(chainback) { 341 if (chainback._constructor.__gcncache__[chainback.__gcnhash__]) { 342 delete chainback._constructor.__gcncache__[chainback.__gcnhash__]; 343 } 344 } 345 346 /** 347 * Maps constructs, that were fetched via the Rest API, to a hashmap, using 348 * their keyword as the keys. 349 * 350 * @param {object<string, object>} constructs Consturcts mapped against 351 * their id. 352 * @return {object<string, object>} Constructs mapped against their keys. 353 */ 354 function mapConstructs(constructs) { 355 if (!constructs) { 356 return {}; 357 } 358 var map = {}; 359 var constructId; 360 for (constructId in constructs) { 361 if (constructs.hasOwnProperty(constructId)) { 362 map[constructs[constructId].keyword] = constructs[constructId]; 363 } 364 } 365 return map; 366 } 367 368 GCN.uniqueId = uniqueId; 369 GCN.escapePropertyName = escapePropertyName; 370 GCN.renderOnto = renderOnto; 371 GCN.decache = decache; 372 GCN.mapConstructs = mapConstructs; 373 374 }(GCN)); 375