-
Notifications
You must be signed in to change notification settings - Fork 0
/
stencil.js
772 lines (713 loc) · 41.7 KB
/
stencil.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
/**
* @preserve stencil.js
* version 18.1 - 20 Jun 2015
* Kingston Chan - Released under the MIT licence
* https://github.com/kgston/stencil.JS
*/
var stencil = stencil || {};
stencil.opts = $.extend(stencil.opts || {}, (function() {
return {
debug: false,
defaultOutputElement: "stencil-output",
fetchTimeout: 30000,
ieNs: ""
};
}()));
stencil.flags = $.extend(stencil.flags || {}, (function() {
return {
escapeHtml: true
};
}()));
stencil.attributes = $.extend(stencil.attributes || {}, (function() {
return {
destination: "data-stencil-destination",
childStencils: "data-stencil-childs",
recurse: "data-stencil-recurse",
selector: "data-stencil-selector",
imgsrc: "data-stencil-imgsrc"
};
}()));
stencil = $.extend(stencil || {}, (function() {
return {
stencilHolder: {},
/*
Fetches a new stencil asyncronously via AJAX through a valid URL. The response should comprise of 1 or more stencil templates.
Returns a jQuery promise. Set a callback to the progress event to get a stencil object one by one in order defined in the response or to the done event to get a array of all stencil objects.
*/
//url - String
fetch: function(url, destination) {
if (url == null) {
stencil.util.log("Stencil: No valid URL was passed in");
return;
}
var fetchRequest = $.Deferred();
$.ajax({
url: url,
method: "GET",
dataType: "html",
timeout: stencil.opts.fetchTimeout
}).done(function(response) {
//Sanitize HTML comments from the response
response = response.replace(/<!--((.|\n)*?)-->/gm, "");
var $templates = $(response).filter("stencil");
if ($templates.length === 0) {
stencil.util.log("Stencil: No stencils found at <" + url + ">");
}
var templatesList = [];
$templates.each(function(index) {
var $template = $(this);
var outputDestination = destination || $template.attr(stencil.attributes.destination) || "none";
var fetchedTemplate = stencil.define($template.attr("id"), outputDestination, null, $template);
//Manually build recursive links, if any, due to use of the stencilFragment param
stencil.util.buildRecursiveLinkages(fetchedTemplate);
templatesList.push(fetchedTemplate);
//Update the promise
fetchRequest.notify(fetchedTemplate);
});
//Resolve the promise when completed
fetchRequest.resolve(templatesList);
}).fail(function(jqXHR) {
fetchRequest.reject(jqXHR);
stencil.util.log("Stencil: Failed to fetched template at <" + url + "> due to " + jqXHR.status + " - " + jqXHR.statusText);
});
return fetchRequest.promise();
},
/*
Define a new Stencil instance given a
compulsory TagID as a template; which will include the inner HTML of the element the ID points to,
an optional destination, where the it will render to the inner HTML of all elements given with the given class,
If no destination is specified, template will output after the template with its own output container
and optional finalOutputElementType where you can specify a type of element container to hold your output
<stencil> tags
*/
//Element ID - String, optional?Element class - String, optional?Element type - String, private! jQueryFragment
define: function(tagID, destination, finalOutputElementType, stencilFragment) {
//Builds a Stencil object and returns the created instance
//Private method, for internal use only
var getStencil = function(tagID, destination, template) {
return {
guid: stencil.util.guid(), //Unique identifier for a stencil object, uniquely identifies stencils
tagID: tagID, //The original tagID where the template was generated from
template: template, //The actual HTML contents to be duplicated
childStencils: {}, //All child stencils of this parent stencil
isStandard: true, //Is this stencil generated from standard stencil tags or is it a specified child, affect rendering style
destination: destination, //Destination to render the template to
existingContent: "",
clear: function() {
$(this.destination).empty().append(this.existingContent);
},
getChild: function(childID) {
var currentChildStencils = this.childStencils;
var found = null;
if (Object.keys(currentChildStencils).length === 0) {
return null;
}
if (Object.keys(currentChildStencils).indexOf(childID) >= 0) {
found = currentChildStencils[childID];
} else {
Object.keys(currentChildStencils).every(function(childStencil) {
var result = currentChildStencils[childStencil].getChild(childID);
if (result == null) {
return true;
} else {
found = result;
return false;
}
});
}
return found;
},
//Renders out a stencil template into screen given
//compulsory dataset of an array of objects where data will be drawn from to be inserted into the template
//Each object in the array will create a replication of the template.
//Each object should contain all necessary key value pairs to insert into template.
//Keys that are not found will be rendered as blank and logged to console if debug is on.
//Optional output string which control display options. Default: clear output then render
//Implemented: "none", "fragment", "string", "append", "prepend" -> Clear rendering on screen and get DOM output, DOM output
//only leaving output container as-is, literal string output leaving output container as-is, append after
//& append before with output container as-is
//Private parentElem for internal use only. Used to determine its parent during recursive calls and
//also used to determine if its is the first stencil being called.
//JSON dataset - Array[Objects], optional?output - String, private!parentElem - Element]
render: function(dataset, output, parentElem) {
//Check for errors in the dataset and return false early if there is no data
if (!(Array.isArray(dataset))) {
if (stencil.util.isObject(dataset)) {
dataset = [dataset];
} else if (typeof dataset === "undefined") {
stencil.util.log("Stencil - " + this.tagID + ": has no data, skipping...");
return false;
} else {
stencil.util.log("Stencil - " + this.tagID + ": dataset in an invalid format, detected: " + typeof dataset);
return false;
}
} else if (dataset == null || dataset.length === 0) {
stencil.util.log("Stencil - " + this.tagID + ": was not passed in data");
return false;
}
//Init all used variables
var destination = this.destination;
var isDetached = false;
var destinationElem;
//If this is the starting stencil
if (parentElem == null) {
//Search for the destination DOM and get its parent
parentElem = $(destination);
if (destination === "none" || output === "none" || output === "string" || output === "fragment") {
if (output === "none" || output === "string" || output === "fragment") {
destinationElem = $(document.createElement('div'));
} else {
stencil.util.log("Stencil: Only render output type \"none\", \"fragment\" or \"string\" is supported for stencils with no destination");
return false;
}
} else {
//Make a clone of one of the destination (in case there may be more than 1)
//Don't empty it yet as we may need the data in the element
//From here on out, it will add changes to the cloned element which is separate fragment
//from the page DOM
destinationElem = parentElem.filter(":first").clone();
}
isDetached = true;
} else {
//Otherwise determine its destination via its parent
//This is to reduce the performance impact from a full search to just a search within its parent
//Added the destination replacement for "\" for cases where specificInner id="foo{{lpIdx}}" and later declared as "foo\\{\\{lpidx\\}\\}"
//due to jQuery limitation, the resulting destination becomes "foo\{\{lpidx\}\}". So the destination needs 3 final backslashes to find
//the destination
destinationElem = parentElem.find(destination.replace(/\\(?=\{|\})/g, "\\\\\\"));
}
var template = this.template;
var childStencils = this.childStencils;
var isStandard = this.isStandard;
//If destination is valid...
if (destinationElem.length) {
//And if output === ...
if (output === "append" || output === "prepend") {
//Copy contents for appending after template generation and empty
this.existingContent = destinationElem.html();
destinationElem.empty();
} else if (output === "none") {
//None behaviour is to empty the output and return the fragement
parentElem.empty();
destinationElem.empty();
} else if (output === "string" || output === "fragment") {
//Else Fragment or String behaviour is to leave the output alone and return the fragement
} else {
destinationElem.empty();
}
} else {
//Otherwise log error and return
stencil.util.log("Stencil: Destination ID - " + destination + " not found!");
return false;
}
//For each set of Object | 1 Object = 1 template generated
dataset.forEach(function(param, loopIdx) {
//Insert data into placeholder
param.lpIdx = loopIdx; //Special key for loop index
param.ctIdx = loopIdx + 1; //Special key for counter index
var curOutput = template;
//Search through whole doc for {{*}} items
var variableRegex = /\{\{[\w.\[\]/]*\}\}/;
while (curOutput.search(variableRegex) >= 0) {
//Get the key
var keyString = curOutput.match(variableRegex)[0];
var keyParts = stencil.util.getKeyParts(keyString);
var paramValue = stencil.util.getKeyValue(param, keyParts);
//Replace the value
if(paramValue == null) paramValue = ""; //Prevent 0 issue
curOutput = curOutput.replace(keyString, paramValue);
}
var curDestElem = destinationElem; //Copy of destination
//If template is not a child stencil...
if (isStandard) {
//Create a wrapper for this iteration and insert it as a child of the destination
var wrapperGuid = stencil.util.createWrapper(destinationElem, "stencil-replicator", "append");
curDestElem = destinationElem.find(wrapperGuid); //Set output to this iteration wrapper
}
//Output main to screen
curDestElem.append(curOutput);
//Remove the GUID class that is used as a destination for non standard child stencils
if (!isStandard) {
curDestElem.removeClass(destination.replace(".", ""));
}
//If childStencils has stencils, render those after the parentStencil has been drawn
Object.keys(childStencils).forEach(function(key) {
//Copy global params into next param set
var nextParamSet = param[key];
if (param.global != null) {
//Check if the param set is an array or an object and allocate accordingly
if (Array.isArray(nextParamSet)) {
nextParamSet.forEach(function(obj) {
obj.global = param.global;
});
} else if (stencil.util.isObject(nextParamSet)) {
nextParamSet.global = param.global;
}
}
//Render the inner stencil
childStencils[key].render(nextParamSet, null, curDestElem);
});
//Render post options after child stencils has been rendered as option values may be generated in a child stencil
curDestElem.find("select[" + stencil.attributes.selector + "]").each(function() {
//Get the selectionID to get the correct data
var selectionID = this.getAttribute(stencil.attributes.selector);
var keyParts = stencil.util.getKeyParts(selectionID);
//Select the correct option as given in JSON dataset
stencil.util.selectOption($(this), stencil.util.getKeyValue(param, keyParts));
});
curDestElem.find("img[" + stencil.attributes.imgsrc + "]").each(function() {
//Get the imgSrc string to populate into the src attr and add the /noEsc flag to prevent / from getting escaped
var imgSrc = this.getAttribute(stencil.attributes.imgsrc) + "/noEsc";
var keyParts = stencil.util.getKeyParts(imgSrc);
this.setAttribute("src", stencil.util.getKeyValue(param, keyParts));
this.removeAttribute(stencil.attributes.imgsrc);
});
});
//If output option is prepend, put the data back in now
if (output === "prepend") {
destinationElem.append(this.existingContent);
}
//Attached detached first stencil back into page after rendering everything
//Unless user does not need it to be displayed
if (isDetached) {
//Clean up all stencil related tags and convert outermost stencil-output tag to div
//This is to reduce DOM clutter and conform to W3C HTML standards (the stencil-output tag still
//does not conform, but you can change that under stencil.opts.defaultOutputElement or set
//it during define as the 4th parameter)
//if(!stencil.opts.debug) {
stencil.util.cleanUpStencils(destinationElem);
//}
//If output === "none", return output node to user
if (output === "none" || output === "fragment") {
//parentElem.replaceWith(destinationElem.clone().empty()); Depreciated as data is already cleared in the beginning
return destinationElem.children();
} else if (output === "string") {
return destinationElem.html();
} else if (output !== "append") {
//Empty the output container first if its not appending
parentElem.empty();
}
parentElem.append(destinationElem.children());
return true;
}
}
};
};
//Creates a deep clone of a stencil object
var clone = function(existingStencil) {
//First get a shallow clone of the object
var newStencil = getStencil(existingStencil.tagID, existingStencil.destination, existingStencil.template);
//Do a deep clone of all child Stencils
Object.keys(existingStencil.childStencils).forEach(function(childStencilName) {
newStencil.childStencils[childStencilName] = clone(existingStencil.childStencils[childStencilName]);
});
return newStencil;
};
var specificInners;
//Return a existing stencil if already initialized because template is already removed once initalized
if (stencil.stencilHolder[tagID] != null && stencilFragment == null) {
var existingStencil = stencil.stencilHolder[tagID];
//Replicate the existingStencil
var newStencil = clone(existingStencil);
//If there is no destination, create a new wrapper and save it as local destination
if (destination == null) {
destination = stencil.util
.createWrapper(
$(existingStencil.destination),
finalOutputElementType || stencil.opts.defaultOutputElement,
"after");
}
//Set the local destination as the template destination, either from above code or user defined destination
newStencil.destination = destination;
return newStencil;
}
//If no stencilFragment specified, get html from tagID
var isParent = (stencilFragment)? false: true;
var tagElem = stencilFragment || $("#" + stencil.util.escapeCSS(tagID));
if (!tagElem.length) {
stencil.util.log("Stencil: Tag ID '" + tagID + "' provided is not found!");
return null;
} else {
//Check for a specified destination defined in the stencil tag
if (destination == null) { destination = tagElem.attr(stencil.attributes.destination); }
//If a destination is not specified, insert the output after the template.
if (destination == null) {
destination = stencil.util.createWrapper(
tagElem,
finalOutputElementType || stencil.opts.defaultOutputElement,
"after");
}
stencilFragment = tagElem.detach();
}
//Check if childStencil attribute exists and set it into the specific inners array
var stencilChildAttr = stencilFragment.attr(stencil.attributes.childStencils);
if (stencilChildAttr != null && stencilChildAttr != "") {
if (specificInners == null) {
specificInners = [];
}
stencilChildAttr.split(" ").forEach(function(childStencilID) {
if (childStencilID != "") {
specificInners.push(childStencilID);
}
});
}
//Otherwise build a new stencil
var childStencils = {};
//Build all standard childStencils first
stencil.util.log("Stencil: Searching for child stencils in " + tagID);
//If there are more descendent stencil tags, recursively define them
//and attach them as child of their parents
if (stencilFragment.find("stencil").length !== 0) {
childStencils = stencil.util.findStencils(stencilFragment);
}
stencil.util.log("Stencil: Completed child stencil generation for " + tagID + ", " +
Object.keys(childStencils).length + " child stencil");
var buildChildStencils = function buildChildStencils($parentStencil) {
var childStencils = {};
var parentChildAttr = $parentStencil.attr(stencil.attributes.childStencils);
if (parentChildAttr != null && parentChildAttr != "") {
parentChildAttr.split(" ").forEach(function(childStencilID) {
if (childStencilID != "") {
stencil.util.log("Stencil: Building specified child stencil: " + childStencilID);
var $childTag = $parentStencil.find("#" + stencil.util.escapeCSS(childStencilID));
if (!$childTag.length) {
stencil.util.log("Stencil: Specified child stencil: " + childStencilID + " not found!");
return;
}
//Changed this from ID to class selector because when you replicate multiple copies of the parent
//stencil, it will cause issues with ID selector as there are multiple tags with the same ID
//May consider removing innerTagID from ID to clean up
//Use a GUID as a destination address to prevent mix up from tagID as it may be a stencil variable
var newDestination = stencil.util.guid();
$childTag.addClass(newDestination);
var innerDestination = "." + newDestination; //Convert to JQuery class selector format
var grandchildStencils = buildChildStencils($childTag);
childStencils[childStencilID] = getStencil(childStencilID, innerDestination, $childTag.html());
childStencils[childStencilID].childStencils = grandchildStencils;
childStencils[childStencilID].isStandard = false;
$childTag.empty();
}
});
}
return childStencils;
};
//Check if there are any non standard specific child stencils
childStencils = $.extend(childStencils, buildChildStencils(stencilFragment));
//Build the parentStencil
var parentStencil = getStencil(tagID, destination, stencilFragment.html());
//Set all the child stencils built earlier
parentStencil.childStencils = childStencils;
//Remove the stencil template
//Only can remove at the end because it needs to build the childStencils first before removing
//stencilFragment.remove();
//Save to the holder before returning stencil object
stencil.stencilHolder[tagID] = parentStencil;
//If is the main parent stencil, relink all recursive stencils
if(isParent) {
stencil.util.buildRecursiveLinkages(parentStencil);
}
return parentStencil;
},
//Auto compile function to scan entire page for stencil templates
//Returns an object of tagID & stencil object pairs
build: function(startElementID) {
startElementID = (startElementID == null) ? $("body") : $("#" + startElementID);
if (startElementID.length === 0) {
stencil.util.log("Stencil: Unable to start build process as starting element ID cannot be found");
return null;
}
return stencil.util.findStencils(startElementID);
},
util: {
//Creates a new GUID
guid: function() {
//Retrieved from http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
var d = new Date().getTime();
if (window.performance && typeof window.performance.now === "function") {
d += performance.now(); //use high-precision timer if available
}
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return "stencil-" + uuid;
},
//Selects the given value in the drop down box for the given select element
selectOption: function(selectElement, valueToSelect) {
selectElement.children().each(function() {
if (this.value == valueToSelect) {
//Added selected attribute this way as jQuery clone option does not clone properties
//Creates issues when using the returned HTML code from render
this.setAttribute("selected", "");
return false;
}
});
selectElement.removeAttr(stencil.attributes.selector);
},
//Returns the outerHTML of the given HTML element
outerHTML: function(element) {
return element.clone().wrap('<p>').parent().html();
},
//Create a new wrapper HTML after the element provided of the given wrapperName
//Location is a element and wrapperName is a string referring to the wrapper name
//creationType is a function name that will control the creation type, e.g. append, after, replaceWith...
//Returns a valid jQuery selector string to the elements created
createWrapper: function(locationElement, wrapperName, creationType) {
var classID = stencil.util.guid();
var newWrapper = document.createElement(stencil.opts.ieNs + wrapperName);
newWrapper.className = (newWrapper.className === "") ? classID : newWrapper.className + " " + classID;
locationElement[creationType](newWrapper);
return stencil.util.escapeCSS(stencil.opts.ieNs) + wrapperName + "." + classID; //Convert to JQuery class selector format
},
//Finds all next level stencil tags and not further
findStencils: function(startElement) {
var stencils = {};
function search(startElement, collection) {
var isStencilTag = function(element) {
if (element.nodeName === "STENCIL" || element.nodeName === "stencil") {
return true;
} else {
return false;
}
};
//For each child in the starting node
var childNodes = startElement.childNodes;
for (var i = 0; i < childNodes.length; i++) {
if (!isStencilTag(childNodes[i])) {
//If its not a stencil tag, continue down the tree
search(childNodes[i], collection);
} else if(childNodes[i].attributes[stencil.attributes.recurse] != null) {
//If it is a recurse tag, create a placeholder for the linkage builder
var tempRecurseDestination = stencil.util.createWrapper(
$(childNodes[i]),
stencil.opts.defaultOutputElement,
"after"
);
collection[childNodes[i].attributes[stencil.attributes.recurse].value] = {
recurse: true,
tempDestination: tempRecurseDestination
}
childNodes[i].remove();
} else {
//Otherwise define the stencil at this node and stop for this branch
collection[childNodes[i].id] = stencil.define(childNodes[i].id, null, "stencil-output", $(childNodes[i]));
}
}
}
search(startElement.get(0), stencils);
return stencils;
},
/*
Runs through the childStencils in the supplied parent stencil and looks for recursive placeholders. It then relinks these placeholders with the actual stencil and replaces the destination in the template with the actual output destination
*/
buildRecursiveLinkages: function(parentStencil) {
Object.keys(parentStencil.childStencils).forEach(function(childKey) {
var childStencil = parentStencil.childStencils[childKey]
if(childStencil.recurse) {
//Replace the temp obj with the actual stencil, forming a cyclic loop
parentStencil.childStencils[childKey] = stencil.stencilHolder[childKey];
//Replace the output destination in the template to match with the real stencil
parentStencil.template = parentStencil.template.replace(
childStencil.tempDestination.split(".")[1],
parentStencil.childStencils[childKey].destination.split(".")[1]
);
} else {
stencil.util.buildRecursiveLinkages(childStencil);
}
});
},
//Cleans up all the stencil generated DOMs that will not be used after template generation
cleanUpStencils: function(startElement) {
function unwrapper(startElement) {
var isStencilTypeTag = function(element) {
if (element.nodeName.indexOf("STENCIL") >= 0 ||
element.nodeName.indexOf("stencil") >= 0) {
return true;
} else {
return false;
}
};
//If it is a stencil generated element
if (isStencilTypeTag(startElement)) {
//It may be possible that there are no childs in the stencil tags, if so, just remove the whole node
if (startElement.childNodes.length === 0) {
startElement.parentNode.removeChild(startElement);
}
//Unwrap this element and then run the unwrapper for each of the child nodes
//Very tedious to convert this from jQuery to pure Javascript, probably causes about 5-10% render time
//Most expensive is the jQuery instantiation...
$(startElement).children().unwrap().each(function() {
unwrapper(this);
});
} else {
//Otherwise, don't unwrap and just continue recursively
var childNodes = startElement.childNodes;
for (var i = 0; i < childNodes.length; i++) {
unwrapper(childNodes[i]);
}
}
}
//Start unwrapping from the child elements and leave the main parent alone
//Issue with unwrapping the root node in a document fragment
startElement.children().each(function() {
unwrapper(this);
});
},
escapeCSS: function(value) {
/*! http://mths.be/cssescape v0.2.1 by @mathias | MIT license */
// http://dev.w3.org/csswg/cssom/#serialize-an-identifier
var string = String(value);
var length = string.length;
var index = -1;
var codeUnit;
var result = '';
var firstCodeUnit = string.charCodeAt(0);
while (++index < length) {
codeUnit = string.charCodeAt(index);
// Note: there’s no need to special-case astral symbols, surrogate
// pairs, or lone surrogates.
// If the character is NULL (U+0000), then throw an
// `InvalidCharacterError` exception and terminate these steps.
if (codeUnit == 0x0000) {
throw new InvalidCharacterError(
'Invalid character: the input contains U+0000.'
);
}
if (
// If the character is in the range [\1-\1F] (U+0001 to U+001F) or is
// U+007F, […]
(codeUnit >= 0x0001 && codeUnit <= 0x001F) || codeUnit == 0x007F ||
// If the character is the first character and is in the range [0-9]
// (U+0030 to U+0039), […]
(index == 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
// If the character is the second character and is in the range [0-9]
// (U+0030 to U+0039) and the first character is a `-` (U+002D), […]
(
index == 1 &&
codeUnit >= 0x0030 && codeUnit <= 0x0039 &&
firstCodeUnit == 0x002D
)
) {
// http://dev.w3.org/csswg/cssom/#escape-a-character-as-code-point
result += '\\' + codeUnit.toString(16) + ' ';
continue;
}
// If the character is not handled by one of the above rules and is
// greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or
// is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to
// U+005A), or [a-z] (U+0061 to U+007A), […]
if (
codeUnit >= 0x0080 ||
codeUnit == 0x002D ||
codeUnit == 0x005F ||
codeUnit >= 0x0030 && codeUnit <= 0x0039 ||
codeUnit >= 0x0041 && codeUnit <= 0x005A ||
codeUnit >= 0x0061 && codeUnit <= 0x007A
) {
// the character itself
result += string.charAt(index);
continue;
}
// Otherwise, the escaped character.
// http://dev.w3.org/csswg/cssom/#escape-a-character
result += '\\' + string.charAt(index);
}
return result;
},
//Escapes all HTML special characters
//https://github.com/janl/mustache.js/blob/master/mustache.js#L82
escapeHTML: function(dirtyValue) {
if(dirtyValue == null) {
return dirtyValue;
}
var entityMap = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
"/": "/"
};
return String(dirtyValue).replace(/[&<>"'\/]/g, function(specialChar) {
return entityMap[specialChar];
});
},
//Cleans the raw key input and returns the key and flags associated with it
getKeyParts: function(keyString) {
keyParts = keyString.replace(/\{|\}/g, "").split("/");
return {
key: keyParts.shift(),
flags: keyParts
};
},
//Gets the value from a complex object via Javascript object notation and applies flags on the dataset value
getKeyValue: function(dataset, keyParts) {
var value = JSON.parse(JSON.stringify(dataset));
//Drill down search for placeholders with foo.bar.key notation
keyParts.key.split(/[\.\[\]]/).every(function(keyPath, splitIdx) {
if (keyPath === "") {
return true;
} else {
value = (value[keyPath] == null)? stencil.util.log("Stencil: " + keyPath + " of " + keyParts.key + " not found"): value[keyPath];
if (value != null) {
return true;
} else {
return false;
}
}
});
return executeFlag(value, keyParts.flags);
//Executes all variable flags for this key-value pair and returns the final value
function executeFlag(value, flags) {
//Flag executor is a grouping of flag groups containing each flag's logic and a compulsory trigger callback that will execute any flag group logic
//Considering to upgrade to ES6 Map when support is more widespread to suppored ordered flag groups
var flagExecutor = {
escapeHtml: {
esc: function escapeHtmlEsc() {
return stencil.util.escapeHTML(value);
},
noEsc: function escapeHtmlNoEsc() {
return value;
},
default: function escapeHtmlDefault() {
if(stencil.flags.escapeHtml) return this.esc();
else return this.noEsc();
},
trigger: function escapeHtmlTrigger() {
if(flags.indexOf("esc") >= 0) return this.esc();
else if(flags.indexOf("noEsc") >= 0) return this.noEsc();
else return this.default();
}
}
}
Object.keys(flagExecutor).forEach(function executeFlagGroups(flagGroup) {
value = flagExecutor[flagGroup].trigger();
});
return value;
}
},
isIE: function() {
var myNav = navigator.userAgent.toLowerCase();
return (myNav.indexOf('msie') != -1) ? parseInt(myNav.split('msie')[1]) : false;
},
isObject: function(obj) {
return obj === Object(obj);
},
log: function(content) {
if (stencil.opts.debug) {
console.log(content);
}
}
}
};
}()));
(function ieNamespaceFix() {
var isIE = stencil.util.isIE();
if (isIE != false && isIE < 10) {
document.createElement("stencil");
document.createElement("stencil-replicator");
document.createElement("stencil-output");
stencil.opts.ieNs = "STENCIL:";
}
}());