Bar chart plugin - IE9-110 issue with chart expanding outside of container - javascript

I've made a simple bar chart plugin which takes some settings and data and displays it as a percentage, it's working perfectly fine in all browsers except IE9-10.
The chart still displays fine yet there is a weird 'jump' once each of the bars have finished animating where the table seems to gain additional height. I've measured the height of the bars in the chart in browsers where it's working fine and there appears to be an additional ~50px height being added in browsers where the weird 'jump' effect occurs.
Here is the relevant CSS:
#barchart {
text-align: center;
}
.bar {
margin-right: 10px;
position: relative;
bottom: 0;
overflow: hidden;
padding-top: 50px;
height: 100%;
}
.bar .label {
text-align: center;
width: inherit;
margin: 0 auto 20px auto;
width: 116px;
}
.bar__value {
height: 0%;
background-repeat: repeat-y;
background-position: top center;
margin: 0 5px;
}
And here is the JS:
var Crafted = (function(c) {
return c;
})(Crafted || {});
(function($, c) {
c.BarChart = (function() {
var barChart = function(target, options, data) {
this.target = target;
this.ChartItem(options);
this.data = data;
this.create();
};
barChart.prototype.ChartItem = function(options) {
var settings = $.extend({
width: '100%',
height: '500px',
usePercentSymbol: false,
delay: 1000,
animSpeed: 1000,
chartImage: '',
chartBgColour: '#CCCCCC'
}, options);
this.width = settings.width;
this.height = settings.height;
this.usePercentSymbol = settings.usePercentSymbol;
this.delay = settings.delay;
this.animSpeed = settings.animSpeed;
this.chartImage = settings.chartImage;
this.chartBgColour = settings.chartBgColour;
return this;
};
barChart.prototype.create = function() {
var _self = this;
if (!_self.target) {
console.error('Error: BarChart \'target\' must be specified');
return;
}
if (!_self.data) {
console.error('Error: BarChart \'data\' must be provided');
return;
}
var $barChart = $('<table></table>').attr('id', 'barchart');
var $charts = $('<tr></tr>').addClass('data-row');
$charts.appendTo($barChart);
$barChart.appendTo(_self.target);
$barChart.attr('width', _self.width)
.attr('height', _self.height);
$.each(_self.data, function(index, value) {
var $chart = $('<td></td>')
.addClass('bar')
.attr('valign', 'bottom')
.attr('data-percent', value.percent);
$chart.appendTo($charts);
var $chartLabel = $('<div></div>').addClass('label');
$chartLabel.appendTo($chart);
var $chartValue = $('<div></div>').addClass('label__percent').text(_self.usePercentSymbol ? '0%' : 0);
$chartValue.appendTo($chartLabel);
var $chartTitle = $('<div></div>').addClass('label__title').text(value.label);
$chartTitle.appendTo($chartLabel);
var $barValue = $('<div></div>').addClass('bar__value');
var barStyle = _self.chartImage ?
'background-image:url(\'' + _self.chartImage + '\');' :
'background-color:' + _self.chartBgColour
$barValue.attr('style', barStyle);
$barValue.appendTo($chart);
});
setTimeout(function() {
$('.bar').each(function() {
var percentage = $(this).attr('data-percent');
var $percentLbl = $(this).find('.label__percent');
$(this).children('.bar__value').animate({
height: percentage + '%'
}, _self.animSpeed);
$({
countNum: 0
}).animate({
countNum: percentage
}, {
duration: _self.animSpeed,
easing: 'linear',
progress: function() {
var currentValue = Math.floor(this.countNum);
$percentLbl.text(_self.usePercentSymbol ? currentValue + '%' : currentValue);
}
});
});
}, _self.delay);
};
return barChart;
})();
})(jQuery, Crafted);
$(function() {
(function(c) {
var settings = {
width: '800px',
height: '400px',
usePercentSymbol: true,
delay: 200,
animSpeed: 1000,
chartImage: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/662693/chimney.svg'
};
var data = [{
label: 'MANCHESTER',
percent: 78
}, {
label: 'BIRMINGHAM',
percent: 69
}, {
label: 'LONDON',
percent: 94
}, {
label: 'CARDIFF',
percent: 39
}, {
label: 'GLASGOW',
percent: 54
}, {
label: 'BELFAST',
percent: 35
}]
var barChart = new c.BarChart('#barChart', settings, data);
})(Crafted);
});
I have a JsFiddle that demonstrates the problem. If you load this in IE9/10 (you can use the browser emulator in IE dev tools - F12) you will see the strange effect I'm talking about. This doesn't occur in IE11/Edge etc...
Could it be due to the padding top applied to the <td> elements? This is used to give enough spacing for each chart label to prevent them from being cut off.

try with this css modification to bar class
.bar {
margin-right: 10px;
position: relative;
bottom: 0;
overflow: hidden;
padding-top: 50px;
height: 75%;
}
seems it works either on ie9/10 and chrome

Related

A div is bigger than what I made it

I am making a John Conway's game of life and when I try run it by pressing space, it is actually bigger than what the console shows it to be. I set the div to be exactly 0.1% of the bigger grid which is in dark blue. But it seems to just be bigger or smaller. I also use panning and zooming for the project
//Important variable
var mainGrid = document.querySelector(".main-grid")
var windowWidth = document.documentElement.clientWidth
var windowHeight = document.documentElement.clientHeight
var time = 500
var Data = {
livingCells: [
[3,0],
[5,0],
[4,1],
[5,1]
]
}
//functions
var updateCells = (rle) => {
if (!rle) {
$(".main-grid").empty()
for (let i = 0; i < Data.livingCells.length; i++) {
const element = Data.livingCells[i];
$(".main-grid").append('<div id="'+i+'" class="on"></div>')
$('#'+i+'').css({ 'left': element[0]/10+'%', 'top': element[1]/10+'%' ,})
console.log(element[1])
}
}
}
var ID;
var cellInterval = () => {
ID = setInterval(() => {
updateCells()
}, time)
}
var intervalOn = false;
//listeners
document.body.onkeyup = function(e) {
if (e.key == " " ||
e.code == "Space" ||
e.keyCode == 32
) {
if(!intervalOn){
cellInterval()
intervalOn = true;
console.log("interval on")
} else {
clearInterval(ID)
intervalOn = false;
console.log("interval off")
}
}
}
// panzoom
panzoom(mainGrid, {
minZoom: 0.3,
maxZoom: 10,
initialZoom: 1.5,
initialX: mainGrid.offsetWidth/2,
initialY: mainGrid.offsetWidth,
bounds: true,
boundsPadding: 0.3
});
* {
padding: 0;
margin: 0;
outline: 0;
overflow: hidden;
}
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: gray;
}
.main-grid{
width: 100vw;
height: 100vw;
background-color: rgb(0, 0, 0);
}
.hover:hover{
transition: 0.3s;
background-color: rgb(79, 124, 182);
transform: scale(1.2);
}
.on{
position: fixed;
height: 0.1%;
width: 0.1%;
background-color: white;
}
<body>
<div class="main-grid">
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="crossorigin="anonymous"></script>
<script src='https://unpkg.com/panzoom#9.4.0/dist/panzoom.min.js'></script>
<script src="app.js"></script>
</body>
Have you tried Include
{
box-sizing: border-box;
}

Incorrect height value after transition in eventListener

var container = document.querySelector(".container");
var bar = document.querySelector(".bar");
var barL = document.getElementsByClassName("barLayer");
var value = document.getElementsByClassName("value");
var compare = document.getElementsByClassName("compare"); // array vallue to show than code can't get the correct result of that bar's height
var pixelStep = 4; // cooficient to expand bar height. if val=100 => bar.height = 400px
var barNum = 12; // number of bars
var spaceBetween = 100; // space between bars
var intervalTime = 10; // ms reading transition changes
var delayTime = 50; // ms after drawing previous bar.
const data = [
{ brand : "SAMSUNG", val : 40, color : "#c00"},
{ brand : "APPLE", val : 46, color : "#0c0"},
{ brand : "HUAWEI", val : 43, color : "#f84"},
{ brand : "ASUS", val : 71, color : "#248"},
{ brand : "XIAOMI", val : 53, color : "#0cc"},
{ brand : "OPPO", val : 100, color : "#cc0"},
{ brand : "VIVO", val : 66, color : "#c0c"},
{ brand : "MOTOROLA", val : 86, color : "#ccc"},
{ brand : "LENOVO", val : 61, color : "#c40"},
{ brand : "LG", val : 93, color : "#333"},
{ brand : "NOKIA", val : 83, color : "#088"},
{ brand : "OTHERS", val : 51, color : "#06c"} ];
sortFunction(data, "val"); //sort Array
clone(bar, container); // clone bar for (barNum-1) times
for(let i = 0; i < data.length; i++){ barL[i].style.backgroundColor = data[i].color; } // colorize every bar with corresponding array color value
//---------------
var myInterval = [];
function anim(j){ // draw bars one after the other in sequence
const computedStyleObj = getComputedStyle(barL[j]);
value[j].textContent = Math.round(parseInt(computedStyleObj.height)/pixelStep) + "K";
compare[j].textContent = data[j].val;
barL[j].style.height = value[j].style.bottom = (data[j].val * pixelStep) + "px" //transiton value of height
//console.log("j : ", data[j].val + " - " + parseInt(computedStyleObj.height));
barL[j].addEventListener("transitionEnd", () =>{clearInterval(myInterval[j]);}); // when transition ends, clear interval
barL[j].addEventListener("webkitTransitionEnd", () =>{clearInterval(myInterval[j]);});
}
for (let i = 0; i < data.length; i++) {
setTimeout(function() {
myInterval[i] = setInterval(function() {anim(i);}, intervalTime); // after delayTime ms start the next bar animation
}, i * delayTime);
}
//----------------
function clone(item, container) { // clone an item in a container function
for(let i = 0; i < barNum-1 ; i++){
var cln = item.cloneNode(true);
container.appendChild(cln);
cln.style.left = ((i+1) * spaceBetween ) + "px";
}
}
function sortFunction(arr, key) { // sort an array of objects by given key
arr.sort(function(a, b){
let x = a[key];
let y = b[key];
if (x < y) {return -1;}
if (x > y) {return 1;}
return 0;
});
data.reverse(); // reverse for descending array
}
:root {
--barWidth : 80px;
--color : #aaa;
--contentWidth : 1200px;
}
* {
margin : 0;
padding : 0;
box-sizing : border-box;
}
body {
width : 100%;
display : flex;
align-content : center;
justify-content : center;
flex-direction : column;
background : var(--color);
font-family : 'Roboto Mono', monospace;
}
.container {
position : relative;
width : var(--contentWidth);
height : 500px;
border : 1px solid #0005;
background : #fff4;
margin : 10px auto;
}
.bar {
position : absolute;
width : var(--barWidth);
margin : 10px;
display : inline-block;
border : 0px solid #0005;
bottom : 0px;
}
.barLayer {
position : absolute;
width : var(--barWidth);;
height : 0px;
bottom : 0;
border : 0px solid #0005;
}
.value, .compare {
position : absolute;
width : var(--barWidth);
height : calc(var(--barWidth)/2);
bottom : 0px;
font-size : calc(var(--barWidth)/4);
text-align : center;
border : 0px solid #fff;
line-height : calc(var(--barWidth)/2);
}
.barLayer, .value {
transition : all 1s linear;
}
<div class="container">
<div class="bar">
<div class="barLayer"></div>
<div class="value"></div>
<div class="compare"></div>
</div>
</div>
I want to clear "interval" after transition end (drawing bar with transition animation) not to busy CPU in vain, but I can't get the correct height value.
You can see the result using snipped link below.
Top is wrong value, bottom is the data from array. I tried some arithmetic methods, but want to use "transitionend" eventListener. Thanks! :)
TL;DR: for animation stuff, use requestAnimationFrame
Usually for animation related things, we prefer using requestAnimationFrame than intervals or timeout. You can think of requestAnimationFrame as a setTimeout that will trigger on the next frame. So I'm proposing this as a replacement for your setInterval.
As for the measure of "how far along is this bar" to animate the numbers, I think you'll get a good enough approximation by just watching how much time has elapsed and how long the transition is supposed to last. This will not be precise by any means, but measuring the elements with getComputedStyle or getBoundingClientRect on every frame would be a disaster for fluidity. So this is why I'm using timestamps in watchAnimation.
And for the rest, I didn't change much. Just re-wrote your code because (sorry but) it was a bit of a mess, and way too many things that didn't have anything to do with the problem at hand.
const container = document.querySelector('.container')
const bar = document.querySelector('.bar')
const values = [100, 80, 63, 20]
const DURATION = 1000 //ms
values.forEach((value, i) => {
initItems(value, i)
staggeredStart(value, i)
watchAnimation(value, i)
})
function initItems(value, i) {
let item = container.children.item(i)
if(!item) {
item = bar.cloneNode(true)
container.appendChild(item)
}
// other things to initialize items
}
function staggeredStart(value, i) {
const item = container.children.item(i)
setTimeout(() => {
item.classList.add('animate')
item.addEventListener('transitionend', () => {
item.classList.remove('animate')
}, {once: true, passive: true})
item.style.setProperty('--value', `${100 - value}%`)
}, i * 50)
}
function watchAnimation(value, i) {
const item = container.children.item(i)
const span = item.querySelector('.value')
span.textContent = '0'
let requestAnimationFrameId
let startTime
function onStart(event) {
startTime = event.timeStamp
onFrame()
}
function onEnd() {
cancelAnimationFrame(requestAnimationFrameId)
span.textContent = value
}
function onFrame() {
requestAnimationFrameId = requestAnimationFrame((time) => {
const delta = time - startTime
const ratio = Math.min(Math.max(0, delta / DURATION), 1)
span.textContent = Math.round(value * ratio)
onFrame()
})
}
item.addEventListener('transitionstart', onStart, {once: true, passive: true})
item.addEventListener('transitionend', onEnd, {once: true, passive: true})
}
* {
box-sizing: border-box;
}
.container {
height: 500px;
border: 1px solid #0005;
overflow: hidden;
display: flex;
--bar-width: 80px;
--space-for-value: 40px;
}
.bar {
--value: 100%;
position: relative;
width: var(--bar-width);
height: calc(100% - var(--space-for-value));
top: var(--space-for-value);
transform: translateY(var(--value));
}
.bar:not(:first-child) {
margin-left: 20px;
}
.bar.animate {
transition: transform 1s linear;
}
.layer {
position: absolute;
width: 100%;
height: 100%;
bottom: 0;
background: red;
}
.value {
position: absolute;
width: 100%;
height: var(--space-for-value);
bottom: 100%;
text-align: center;
line-height: var(--space-for-value);
}
<div class="container">
<div class="bar">
<div class="layer"></div>
<div class="value"></div>
</div>
</div>

Owl Carousel 2 - can't change slide on hover

I already tried to swap the functions on owl.carousel.js but it only works when the mouse moves.
var Autoplay = function(scope) {
this.core = scope;
this.core.options = $.extend({}, Autoplay.Defaults, this.core.options);
this.handlers = {
'translated.owl.carousel refreshed.owl.carousel': $.proxy(function() {
this.autoplay();
}, this),
'play.owl.autoplay': $.proxy(function(e, t, s) {
this.play(t, s);
}, this),
'stop.owl.autoplay': $.proxy(function() {
this.stop();
}, this),
'mouseover.owl.autoplay': $.proxy(function() {
if (this.core.settings.autoplayHoverPause) {
this.pause();
}
}, this),
'mouseleave.owl.autoplay': $.proxy(function() {
if (this.core.settings.autoplayHoverPause) {
this.autoplay();
}
}, this)
};
this.core.$element.on(this.handlers);};
Any idea how to make the slideshow work when mouse on top of the image?
When i had this problem, i used this code:
$('.owl-carousel .owl-dot').hover(function() {
$(this).click();
},
function() {}
);
and here my css for dots:
.owl-dot{
position: relative;
padding: 0;
height: 3px;
margin: 0;
float: left;
}
.owl-dot:before{
content: "";
position: absolute;
top: -168px; // the height of image
height: 168px; // the height of image
width: 100%;
left: 0;
z-index: 0;
}
when you will make hover to dots the image will be changing, that's all !!!

Match parent to absolute children? (responsive Carousel)

Have worked out a solution, see the bottom!
I'm experimenting with a responsive carousel (fluid). I have elements stacked on top of each other so that the width can be fluid depending on the width of the parent. The issue is I need the parent to have overflow hidden which is not possible with children that are absolute positioned.
Tip on cleaning up the JS are appreciated too!
Does anyone have any ideas how to improve this or alternatives? Heres the fiddle: http://jsfiddle.net/j35fy/5/
.carousel-wrap {
position: relative;
}
.carousel-item {
position: absolute;
top: 0;
}
$.fn.mwCarousel = function(options) {
//Default settings.
var settings = $.extend({
changeWait: 3000,
changeSpeed: 800,
reveal: false,
slide: true,
autoRotate: true
}, options );
var CHANGE_WAIT = settings.changeWait;
var CHANGE_SPEED = settings.changeSpeed;
var REVEAL = settings.reveal;
var SLIDE = settings.slide;
var AUTO_ROTATE = settings.autoRotate;
var $carouselWrap = $(this);
var SLIDE_COUNT = $carouselWrap.find('.carousel-item').length;
var rotateTimeout;
if (AUTO_ROTATE) {
rotateTimeout = setTimeout(function(){
rotateCarousel(SLIDE_COUNT-1);
}, CHANGE_WAIT);
}
function rotateCarousel(slide) {
if (slide === 0) {
slide = SLIDE_COUNT-1;
rotateTimeout = setTimeout(function(){
$('.carousel-item').css('margin', 0);
$('.carousel-item').show();
}, CHANGE_WAIT);
if (REVEAL) {
$($carouselWrap.find('.carousel-item')[slide]).slideToggle(CHANGE_SPEED);
} else if (SLIDE) {
var carouselItem = $($carouselWrap.find('.carousel-item')[slide]);
carouselItem.show();
var itemWidth = carouselItem.width();
carouselItem.animate({margin: 0}, CHANGE_SPEED);
} else {
$($carouselWrap.find('.carousel-item')[slide]).fadeIn(CHANGE_SPEED);
}
slide = slide+1;
} else {
if (REVEAL) {
$($carouselWrap.find('.carousel-item')[slide]).slideToggle(CHANGE_SPEED);
} else if (SLIDE) {
var carouselItem = $($carouselWrap.find('.carousel-item')[slide]);
var itemWidth = carouselItem.width();
carouselItem.animate({marginLeft: -itemWidth, marginRight: itemWidth}, CHANGE_SPEED);
} else {
$($carouselWrap.find('.carousel-item')[slide]).fadeOut(CHANGE_SPEED);
}
}
rotateTimeout = setTimeout(function(){
rotateCarousel(slide-1);
}, CHANGE_WAIT);
}
}
$('.carousel-wrap').mwCarousel();
Solution
The first slide actually never moves (last one visible) so that one is set to position: static and all works nicely.
I think by just changing your CSS you're actually there:
.carousel-wrap {
position: relative;
overflow:hidden;
height:80%;
width:90%;
}
Demo: http://jsfiddle.net/robschmuecker/j35fy/2/
Discovered the solution is in fact simple, as the first slide in the DOM (the last you see) never actually moves itself I can set that one slide to be position: static and thus the carousel wrap will set it's height accordingly.
http://jsfiddle.net/j35fy/7/
.container {
background: aliceblue;
padding: 3em;
}
.carousel-wrap {
position: relative;
overflow:hidden;
}
.carousel-item:first-child {
position:static;
}
.carousel-item {
position: absolute;
top: 0;
width: 100%;
}
img {
width: 100%;
}

Internet Explorer: Issue with Overlay / opacity

I'm trying to create a dialog window using the following CSS:
#blackOverlay {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #000000;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";
filter: alpha(opacity=80);
-moz-opacity: 0.8;
-khtml-opacity: 0.8;
opacity: 0.8;
z-index: 1001;
}
#whiteOverlay {
display: block;
position: absolute;
top: 10%;
left: 10%;
width: 80%;
height: 80%;
z-index:2002;
overflow: auto;
background: #c4e982;
}
and the following JS:
var div = $("<div id='blackOverlay'></div");
$("body").prepend(div);
var div = $("<div id='whiteOverlay'></div");
div.html("Loading......");
var u = "myurl?function=example";
div.load(u);
$("body").prepend(div);
This works correctly in Firefox, Safari, Chrome and Opera.
Unfortunately it fails in IE, at least on version 8.0. The color/opacity is only applied to body and NOT on other DIV's. Instead of "hidding" everything behind the blackOverlay, everything (links, buttons, input fields, ...) is still usable although the loaded content is displayed correctly (in front, center of screen).
Any help is appreciated!
Thank you jduren for pointing me in the right direction. After attempting to handle it in similar way as described here I came up with the following workaround:
function shime() {
jQuery.each(jQuery.browser, function(i) {
if($.browser.msie){
$('div').each(function() {
$(this).addClass("shine");
});
}
});
}
function unshime() {
jQuery.each(jQuery.browser, function(i) {
if($.browser.msie){
$(".shine").each(function() {
$(this).removeClass("shine");
});
}
});
}
And the following CSS:
div.shine {
display: none;
}
I know that it's not the best solution, but I'm getting tired of running in circles due to IE "features".
You need to create what's called an iFrame shim. IE paints controls over everything that isn't windowed so you won't be able to handle this by CSS/HTML hacks alone.
Here is a quick overview of Iframe Shimming http://www.macridesweb.com/oltest/IframeShim.html
Also, the Mootools More library includes an iFrame shim Feature http://mootools.net/docs/more/Utilities/IframeShim as do most popular javascript frameworks that create overlayed UI elements.
This is the IFrame Shim class from mootools more library to give you an idea of what's involved, don't use this as it depends on other Mootoosl classes.
var IframeShim = new Class({
Implements: [Options, Events, Class.Occlude],
options: {
className: 'iframeShim',
src: 'javascript:false;document.write("");',
display: false,
zIndex: null,
margin: 0,
offset: {x: 0, y: 0},
browsers: (Browser.Engine.trident4 || (Browser.Engine.gecko && !Browser.Engine.gecko19 && Browser.Platform.mac))
},
property: 'IframeShim',
initialize: function(element, options){
this.element = document.id(element);
if (this.occlude()) return this.occluded;
this.setOptions(options);
this.makeShim();
return this;
},
makeShim: function(){
if(this.options.browsers){
var zIndex = this.element.getStyle('zIndex').toInt();
if (!zIndex){
zIndex = 1;
var pos = this.element.getStyle('position');
if (pos == 'static' || !pos) this.element.setStyle('position', 'relative');
this.element.setStyle('zIndex', zIndex);
}
zIndex = ($chk(this.options.zIndex) && zIndex > this.options.zIndex) ? this.options.zIndex : zIndex - 1;
if (zIndex < 0) zIndex = 1;
this.shim = new Element('iframe', {
src: this.options.src,
scrolling: 'no',
frameborder: 0,
styles: {
zIndex: zIndex,
position: 'absolute',
border: 'none',
filter: 'progid:DXImageTransform.Microsoft.Alpha(style=0,opacity=0)'
},
'class': this.options.className
}).store('IframeShim', this);
var inject = (function(){
this.shim.inject(this.element, 'after');
this[this.options.display ? 'show' : 'hide']();
this.fireEvent('inject');
}).bind(this);
if (!IframeShim.ready) window.addEvent('load', inject);
else inject();
} else {
this.position = this.hide = this.show = this.dispose = $lambda(this);
}
},
position: function(){
if (!IframeShim.ready || !this.shim) return this;
var size = this.element.measure(function(){
return this.getSize();
});
if (this.options.margin != undefined){
size.x = size.x - (this.options.margin * 2);
size.y = size.y - (this.options.margin * 2);
this.options.offset.x += this.options.margin;
this.options.offset.y += this.options.margin;
}
this.shim.set({width: size.x, height: size.y}).position({
relativeTo: this.element,
offset: this.options.offset
});
return this;
},
hide: function(){
if (this.shim) this.shim.setStyle('display', 'none');
return this;
},
show: function(){
if (this.shim) this.shim.setStyle('display', 'block');
return this.position();
},
dispose: function(){
if (this.shim) this.shim.dispose();
return this;
},
destroy: function(){
if (this.shim) this.shim.destroy();
return this;
}
});
window.addEvent('load', function(){
IframeShim.ready = true;
});

Categories

Resources