I've run into a problem with Dojo 1.10 and need some suggestions on how to figure out the culprit. I have a custom widget, TaskButton, that implements the onMouseDown, onMouseUp, and onClick methods. All three have logging statements. The onMouseDown and onMouseUp always gets called and the correct times and their log statements show up in the console. But, onClick sometimes is never called despite repeatedly clicking in the TaskButton. Most of the time clicking outside the TaskButton then back inside it makes the onClick work but not always. When the onClick does not get called its log statement does not show up in the console.
TaskButton.js custom widget
define([
"dojo/_base/declare",
"dojo/_base/event",
"dojo/_base/lang",
"dojo/dom-class",
"dojo/dom-construct",
"dojo/mouse",
"dojo/on",
"dojo/query",
"dojo/topic",
"dijit/Menu",
"dijit/MenuItem",
"dijit/MenuSeparator",
"dijit/PopupMenuItem",
"dijit/popup",
"dijit/Tooltip",
"dijit/Tree",
"dijit/tree/ForestStoreModel",
"dijit/registry",
"dijit/form/Button",
"dijit/_WidgetBase",
"dijit/_OnDijitClickMixin",
"dijit/_TemplatedMixin",
"dijit/_WidgetsInTemplateMixin",
"dojo/text!./templates/TaskButton.html"
], function(declare, event, lang, domClass, domConstruct, mouse, on, query, topic, Menu, MenuItem, MenuSeparator, PopupMenuItem,
Popup, Tooltip, Tree, ForestStoreModel, registry, button, _WidgetBase, _OnDijitClickMixin, _TemplatedMixin, _WidgetsInTemplateMixin, template){
return declare("TaskButton", [_WidgetBase, _OnDijitClickMixin, _TemplatedMixin, _WidgetsInTemplateMixin, Menu], {
scene:0,
sceneId:0,
target:"",
state:"pending",
cloudCover: false,
cloudPercentage: 0,
targetInterest: false,
hsv: false,
previousState:"pending",
backgroundcolor:"#414141",
templateString:template,
baseClass: "TaskButton",
innerNode:undefined,
cm:null,
theTask:null,
eventHandle:null,
postCreate: function()
{
// Get a DOM node reference for the root of our widget
var domNode = this.domNode;
this.innerNode = domNode.firstChild.nextElementSibling.firstElementChild;
domClass.replace(this.innerFill, "task"+this.state+"Background", "task"+this.state+"Background");
if (this.cloudCover && ((this.state === "Ready") || (this.state === "Unassigned"))) {
domClass.replace(this.innerFill, "task"+"Red"+"Background", "task"+this.state+"Background");
}
this.previousState = this.state;
console.log("getting context menu for Scene-" + this.scene + "ContextMenu");
cm = registry.byId("Scene-" + this.scene + "ContextMenu");
this.own(
on(domNode, "contextmenu", lang.hitch(this, "_showContextMenu"))
);
this.inherited(arguments);
},
startup: function()
{
//Turn off button icons if warranted Must do here after dom nodes built
if (!this.cloudCover)
{
dojo.style(dojo.byId("Scene-"+this.scene+"Cloud"), "display", "none");
}
if (!this.targetInterest)
{
dojo.style(dojo.byId("Scene-"+this.scene+"Target"), "display", "none");
}
if (!this.hsv)
{
dojo.style(dojo.byId("Scene-"+this.scene+"HSV"), "display", "none");
}
this.inherited(arguments);
},
test: function(sceneId)
{
console.log("testing");
if (sceneId != this.scene)
{
domClass.replace("Scene-" + sceneId + "Fill", "taskInnerFill", "taskInnerFillSelected");
}
},
buildRendering: function()
{
console.log("buildRendering scene:" + this.scene);
this.inherited(arguments);
},
//
uninitialize: function()
{
if (this.eventHandle != null)
{
console.log("unsubscribing from event topic");
eventHandle.remove();
eventHandle = null;
}
this.inherited(arguments);
},
//
_onMenuClick: function(event)
{
console.log("menu item clicked");
},
_showContextMenu: function(event) {
console.log("opening context menu for scene:" + this.scene);
this.inherited(arguments);
},
// This is always called
_onMouseDown: function(e)
{
var scene = e.currentTarget.attributes["scene"].value;
if (e.button == 0)
{
console.log("mouse left pressed, scene=" + scene + " button=" + e.button);
domClass.replace("Scene-" + scene + "OuterBorder", "taskOuterBorderPressed", "taskOuterBorder");
}
else if (e.button == 2)
{
console.log("mouse right pressed, scene=" + scene + " button=" + e.button);
domClass.replace("Scene-" + scene + "OuterBorder", "taskOuterBorderPressed", "taskOuterBorder");
}
this.inherited(arguments);
},
// This is always called
_onMouseUp: function(e)
{
var scene = e.currentTarget.attributes["scene"].value;
if (e.button == 0)
{
console.log("mouse left released, scene=" + scene + " button=" + e.button);
}
else if (e.button == 2)
{
console.log("mouse right released, scene=" + scene + " button=" + e.button);
}
domClass.replace("Scene-" + this.scene + "OuterBorder", "taskOuterBorder", "taskOuterBorderPressed");
dijit.hideTooltip(e.currentTarget);
this.inherited(arguments);
},
//
_onMouseEnter: function(e)
{
label = "Scene: " + this.scene + "<BR>State: " + this.state + "<BR>Target: " + this.target;
dijit.showTooltip(label,e.currentTarget);
dijit.popup.close();
this.inherited(arguments);
},
//
_onMouseLeave: function(e)
{
this._onMouseUp("");
this.inherited(arguments);
dijit.hideTooltip(e.currentTarget);
},
// This is what is not always called
_onClick: function(e)
{
var scene = e.currentTarget.attributes["scene"].value;
console.log("scene " + scene + " clicked");
this._publishEvent(this.scene, "clicked");
this.inherited(arguments);
},
//
_onBlur: function(e)
{
dijit.popup.close();
this.inherited(arguments);
},
//
_onContextMenu: function(e)
{
this.inherited(arguments);
this._publishEvent({"scene":this.scene,"sceneId":this.sceneId}, "clicked");
dijit.hideTooltip(e.currentTarget);
var widget = this;
theNode = "TaskButtonContainer" + widget.scene;
console.log("mouse right clicked, scene=" + widget.scene + " target: " + e.target + "current target");
theTask = missionCache.query({"sceneId" : this.scene}).then( function(results) {
theTask = results;
if (widget.state === "Unassigned" || widget.state === "Ready") {
//The context menu should fire to allow assignment
var cb = new dijit.form.ComboBox({style:"width:96%;background-color:#414141;margin-top:4px;margin-bottom:4px;",
name:"usersByTask", placeholder:"Assign this task to: ", store:usersContextMenuCB,
labelAttr: 'name',
searchAttr: 'name',
onChange: function(){
theTask[0].taskStatus = "Assigned";
theTask[0].taskOwner = this.item.userName;
missionCache.put(theTask[0]);
console.log("nothing");
widget.set("state", "Assigned");
assignTask(this.item.userName);
widget.domNode.classList.remove("Unassigned");
widget.domNode.classList.add("Assigned");
widget.domNode.setAttribute("dndtype", "Assigned");
this.destroy();
},
onClose: function() { this.destroy();}
});
cb.toggleDropDown();
dijit.popup.open({parent: widget, popup:cb, around:e.target,
onClose: function(){
dijit.popup.close(cb);
}
});
}
});
},
_publishEvent: function(sceneNumber, eventName)
{
console.log("publishing " + eventName + " for scene " + sceneNumber);
topic.publish("TaskButton/tasks", { scene:sceneNumber, task:this, event:eventName });
},
_setStateAttr: function(newState)
{
if (newState != "")
{
console.log("setting state for scene:" + this.scene + " to " + newState);
this._set("state", newState);
if (this.innerNode !== undefined)
{
domClass.replace(this.innerFill, "task"+newState+"Background", "task"+this.previousState+"Background");
}
this.previousState = this.state;
this.state = newState;
}
this.inherited(arguments);
},
_changeTaskState: function(newState)
{
require(["dijit/registry"], function(registry) {
var node = registry.byId(clickedItem);
if (node !== undefined)
{
node.set("state", newState);
console.log("changed task " + this.clickedItem + " state to " + newState);
}
});
this.inherited(arguments);
},
_menuTaskDetails: function(e)
{
console.log("do task deatils");
}
});
})
There are attach events for all the button actions in the template.
TaskButton.html template:
<li class="dojoDndItem" dndType="${state}" style="border:none;padding:0" data-dojo-props="scene:${scene}">
<div id="TaskButtonContainer-${scene}" widgetid="TaskButtonContainer-${scene}" class="${baseClass}" data-dojo-attach-point="taskButtonContainer"
data-dojo-attach-event="onContextMenu:_showContextMenu">
<div widgetid="Scene-${scene}ContextMenu" data-dojo-type="dijit/Menu" data-dojo-props="contextMenuForWindow:false"
data-dojo-attach-point="contextMenu" targetNodeIds="Scene-${scene}Fill" style="display: none;">
<div data-dojo-type="dijit/MenuItem" data-dojo-attach-event="onClick:_menuTaskDetails">
Task Details
</div>
</div>
<div id="Scene-${scene}OuterBorder" widgetid="Scene-${scene}OuterBorder" class="taskOuterBorder" data-dojo-attach-point="outerBorder" scene="${scene}">
<div id="Scene-${scene}Fill" class="taskInnerFill task${state}Background" data-dojo-attach-point="innerFill" scene="${scene}"
data-dojo-attach-event="onMouseDown:_onMouseDown,onMouseUp:_onMouseUp,onDijitClick:_onClick,onMouseEnter:_onMouseEnter,onMouseLeave:_onMouseLeave,onContextMenu:_onContextMenu,onBlur:_onBlur">
<div id="Scene-${scene}Text" class="taskText" data-dojo-attach-point="text">
<table style="margin:0;padding:0">
<tr>
<td>${scene}</td>
</tr>
<tr>
<td>
<img id="Scene-${scene}Cloud" src="img/cloud.png" alt="Cloud cover" height="21" width="21">
<img id="Scene-${scene}Target" src="img/target.png" alt="ATR" height="21" width="21">
<img id="Scene-${scene}HSV" src="img/HSV.png" alt="HSV" height="21" width="21">
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</li>
I've also cleaned up the code per the suggestions and there is no change in the behavior of the TaskButton's onClick event handler.
Hmm,
There is multiple errors...
you don't need to inherits from _WidgetBase, _TemplatedMixin and _OnDijitClickMixin, they come with Menu
Menu should be the Base (so should be first in the inheritance list)
you should no use domNode.firstChild.nextElementSibling.firstElementChild but instead you should use a data-dojo-attach-point in the template
cm = registry.byId('Scene-' + this.scene + 'ContextMenu'); should be this.cm = registry.byId('Scene-' + this.scene + 'ContextMenu');
dojo namespace should not be used. So dojo.style should be replaced by a require to dojo/dom-style and domStyle.set() and dojo.byId should be replaced by a require to dojo/dom and dom.byId()
dijit namespace should not be used. So dijit.hideTooltip should be replaced by a require to dijit/Tooltip then Tooltip.hide() and dijit.showTooltip should be replaced by Tooltip.show() and dijit.popup.close() should be replaced by a require to dijit/_base/popup then popup.close() and dijit.popup.open() should be replaced by popup.open()
var is missing before label in method _onMouseEnter
in onChange of the dijit.form.ComboBox you call a non existing method assignTask
in _changeTaskState method you require dijit/registry but it is already available. So the extra require is useless
probably many more but I am not there to refactor your code
finally nothing is attached to your _onClick method. I don't event understand how it can be sometimes executed... Butmaybe it is connected using data-dojo-attach-event ? If yes, then please also provide your template.
Try to apply all the changes to cleanup some mistakes, and see if it works better.
If no, please share the template of your button.
define([
'dojo/_base/declare',
'dojo/_base/event',
'dojo/_base/lang',
'dojo/dom',
'dojo/dom-class',
'dojo/dom-construct',
'dojo/dom-style',
'dojo/mouse',
'dojo/on',
'dojo/query',
'dojo/topic',
'dijit/Menu',
'dijit/MenuItem',
'dijit/MenuSeparator',
'dijit/PopupMenuItem',
'dijit/popup',
'dijit/Tooltip',
'dijit/Tree',
'dijit/tree/ForestStoreModel',
'dijit/registry',
'dijit/_base/popup',
'dijit/form/Button',
'dijit/_WidgetBase',
'dijit/_OnDijitClickMixin',
'dijit/_TemplatedMixin',
'dijit/_WidgetsInTemplateMixin',
'dojo/text!./templates/TaskButton.html'
], function(declare, event, lang, dom, domClass, domConstruct, domStyle, mouse, on, query, topic, Menu, MenuItem, MenuSeparator, PopupMenuItem,
Popup, Tooltip, Tree, ForestStoreModel, registry, popup, button, _WidgetBase, _OnDijitClickMixin, _TemplatedMixin, _WidgetsInTemplateMixin, template) {
return declare('TaskButton', [Menu, _WidgetsInTemplateMixin], {
scene: 0,
sceneId: 0,
target: '',
state: 'pending',
cloudCover: false,
cloudPercentage: 0,
targetInterest: false,
hsv: false,
previousState: 'pending',
backgroundcolor: '#414141',
templateString: template,
baseClass: 'TaskButton',
innerNode: undefined,
cm: null,
theTask: null,
eventHandle: null,
postCreate: function() {
// Get a DOM node reference for the root of our widget
var domNode = this.domNode;
this.innerNode = domNode.firstChild.nextElementSibling.firstElementChild;
domClass.replace(this.innerFill, 'task' + this.state + 'Background', 'task' + this.state + 'Background');
if (this.cloudCover && ((this.state === 'Ready') || (this.state === 'Unassigned'))) {
domClass.replace(this.innerFill, 'task' + 'Red' + 'Background', 'task' + this.state + 'Background');
}
this.previousState = this.state;
console.log('getting context menu for Scene-' + this.scene + 'ContextMenu');
cm = registry.byId('Scene-' + this.scene + 'ContextMenu');
this.own(
on(domNode, 'contextmenu', lang.hitch(this, '_showContextMenu'))
);
this.inherited(arguments);
},
startup: function() {
//Turn off button icons if warranted Must do here after dom nodes built
if (!this.cloudCover) {
domStyle.set(dom.byId('Scene-' + this.scene + 'Cloud'), 'display', 'none');
}
if (!this.targetInterest) {
domStyle.set(dom.byId('Scene-' + this.scene + 'Target'), 'display', 'none');
}
if (!this.hsv) {
domStyle.set(dom.byId('Scene-' + this.scene + 'HSV'), 'display', 'none');
}
this.inherited(arguments);
},
test: function(sceneId) {
console.log('testing');
if (sceneId != this.scene) {
domClass.replace('Scene-' + sceneId + 'Fill', 'taskInnerFill', 'taskInnerFillSelected');
}
},
buildRendering: function() {
console.log('buildRendering scene:' + this.scene);
this.inherited(arguments);
},
//
uninitialize: function() {
if (this.eventHandle != null) {
console.log('unsubscribing from event topic');
eventHandle.remove();
eventHandle = null;
}
this.inherited(arguments);
},
//
_onMenuClick: function(event) {
console.log('menu item clicked');
},
_showContextMenu: function(event) {
console.log('opening context menu for scene:' + this.scene);
this.inherited(arguments);
},
// This is always called
_onMouseDown: function(e) {
var scene = e.currentTarget.attributes['scene'].value;
if (e.button == 0) {
console.log('mouse left pressed, scene=' + scene + ' button=' + e.button);
domClass.replace('Scene-' + scene + 'OuterBorder', 'taskOuterBorderPressed', 'taskOuterBorder');
} else if (e.button == 2) {
console.log('mouse right pressed, scene=' + scene + ' button=' + e.button);
domClass.replace('Scene-' + scene + 'OuterBorder', 'taskOuterBorderPressed', 'taskOuterBorder');
}
this.inherited(arguments);
},
// This is always called
_onMouseUp: function(e) {
var scene = e.currentTarget.attributes['scene'].value;
if (e.button == 0) {
console.log('mouse left released, scene=' + scene + ' button=' + e.button);
} else if(e.button == 2) {
console.log('mouse right released, scene=' + scene + ' button=' + e.button);
}
domClass.replace('Scene-' + this.scene + 'OuterBorder', 'taskOuterBorder', 'taskOuterBorderPressed');
Tooltip.hide(e.currentTarget);
this.inherited(arguments);
},
//
_onMouseEnter: function(e) {
var label = 'Scene: ' + this.scene + '<BR>State: ' + this.state + '<BR>Target: ' + this.target;
Tooltip.show(label, e.currentTarget);
popup.close();
this.inherited(arguments);
},
//
_onMouseLeave: function(e) {
this._onMouseUp('');
this.inherited(arguments);
Tooltip.hide(e.currentTarget);
},
// This is what is not always called
_onClick: function(e) {
var scene = e.currentTarget.attributes['scene'].value;
console.log('scene ' + scene + ' clicked');
this._publishEvent(this.scene, 'clicked');
this.inherited(arguments);
},
//
_onBlur: function(e) {
popup.close();
this.inherited(arguments);
},
//
_onContextMenu: function(e) {
this.inherited(arguments);
this._publishEvent({
'scene': this.scene,
'sceneId': this.sceneId
}, 'clicked');
Tooltip.hide(e.currentTarget);
var widget = this;
theNode = 'TaskButtonContainer' + widget.scene;
console.log('mouse right clicked, scene=' + widget.scene + ' target: ' + e.target + 'current target');
theTask = missionCache.query({
'sceneId': this.scene
}).then(function(results) {
theTask = results;
if (widget.state === 'Unassigned' || widget.state === 'Ready') {
//The context menu should fire to allow assignment
var cb = new dijit.form.ComboBox({
style: 'width:96%;background-color:#414141;margin-top:4px;margin-bottom:4px;',
name: 'usersByTask',
placeholder: 'Assign this task to: ',
store: usersContextMenuCB,
labelAttr: 'name',
searchAttr: 'name',
onChange: function() {
theTask[0].taskStatus = 'Assigned';
theTask[0].taskOwner = this.item.userName;
missionCache.put(theTask[0]);
console.log('nothing');
widget.set('state', 'Assigned');
//assignTask(this.item.userName);
widget.domNode.classList.remove('Unassigned');
widget.domNode.classList.add('Assigned');
widget.domNode.setAttribute('dndtype', 'Assigned');
this.destroy();
},
onClose: function() {
this.destroy();
}
});
cb.toggleDropDown();
popup.open({
parent: widget,
popup: cb,
around: e.target,
onClose: function() {
popup.close(cb);
}
});
}
});
},
_publishEvent: function(sceneNumber, eventName) {
console.log('publishing ' + eventName + ' for scene ' + sceneNumber);
topic.publish('TaskButton/tasks', {
scene: sceneNumber,
task: this,
event: eventName
});
},
_setStateAttr: function(newState) {
if (newState != '') {
console.log('setting state for scene:' + this.scene + ' to ' + newState);
this._set('state', newState);
if (this.innerNode !== undefined) {
domClass.replace(this.innerFill, 'task' + newState + 'Background', 'task' + this.previousState + 'Background');
}
this.previousState = this.state;
this.state = newState;
}
this.inherited(arguments);
},
_changeTaskState: function(newState) {
var node = registry.byId(clickedItem);
if (node !== undefined) {
node.set('state', newState);
console.log('changed task ' + this.clickedItem + ' state to ' + newState);
}
this.inherited(arguments);
},
_menuTaskDetails: function(e) {
console.log('do task deatils');
}
});
})
I'm using a CDN to Load Bootstrap.css.
My question is how can i check if CDN bootstrap was loaded/found.
And if it wasn't, then load local Boostrap.
Here's Jquery fallback..
<script type="text/javascript">
Modernizr.load([
{
load: '//cdnjs.cloudflare.com/ajax/libs/jquery/1.10.1/jquery.min.js',
complete: function () {
if ( !window.jQuery ) {
Modernizr.load([
{
load: config.js + 'vendor/jquery-1.10.1.min.js',
complete: function () {
console.log("Local jquery-1.10.1.min.js loaded !");
}
}
]);
} else {
console.log("CDN jquery-1.10.1.min.js loaded !");
}
}
}
]);
</script>
And this is how i load Modernizr than Css:
<script src="//cdnjs.cloudflare.com/ajax/libs/modernizr/2.6.2/modernizr.min.js"></script>
<script type="text/javascript">
if (typeof Modernizr == 'undefined') {
document.write(unescape("%3Cscript src='" + config.js + "/vendor/modernizr-2.6.2-respond-1.1.0.min.js' type='text/javascript'%3E%3C/script%3E"));
console.log("Local Modernizer loaded !");
}
</script>
<script type="text/javascript">
Modernizr.load([
{
load: config.css + "bootstrap.css",
complete: function () {
console.log("bootstrap.css loaded !");
}
},
{
load: config.css + "responsive.css",
complete: function () {
console.log("responsive.css loaded !");
}
},
{
load: config.css + "icons.css",
complete: function () {
console.log("Fontello icons.css loaded !");
}
},
{
load: config.css + "icons-ie7.css",
complete: function () {
console.log("Fontello icons-ie7.css loaded !");
}
},
{
load: config.css + "animation.css",
complete: function () {
console.log("Fontello animation.css loaded !");
}
}
]);
</script>
I have no idea how i could check if the css was loaded.. just like i did with modernizr and Jquery..
Thanks in advance...
Stoyan Stefanov has had a Pure JS solution for this for some time now, which actually just got an update not too long ago. Check out the link for an in depth breakdown.
But style.sheet.cssRules will only be populated once the file is loaded, so by checking for it repeatedly with a setInterval you are able to detect once your CSS file is loaded.
d = document;
d.head || (d.head = d.getElementsByTagName('head')[0]);
var style = d.createElement('style');
style.textContent = '#import "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"';
var fi = setInterval(function() {
try {
style.sheet.cssRules;
console.log('loaded!');
clearInterval(fi);
} catch(e){
console.log('loading...');
}
}, 10);
d.head.appendChild(style);
YepNope acknowledges the use of this technique, though they note in their documentation that YepNope's "callback does not wait for the css to actually be loaded".
Plus with YepNope's recent depreciation, let's move onto to a solution that integrates with Modernizr (who will no longer be including YepNope in their software), but does not utilize any of Modernizr's libraries, since they still do not have a native solution. Offirmo combines a couple of neat techniques from Robert Nyman and Abdul Munim to let Modernizr know the CSS is actually loaded.
We start with the following function, which allows us to get the CSS property of an element:
function getStyle(oElm, strCssRule){
var strValue = "";
if (document.defaultView && document.defaultView.getComputedStyle){
strValue = document.defaultView.getComputedStyle(oElm, "").getPropertyValue(strCssRule);
} else if (oElm.currentStyle){
strCssRule = strCssRule.replace(/\-(\w)/g, function (strMatch, p1){
return p1.toUpperCase();
});
strValue = oElm.currentStyle[strCssRule];
}
return strValue;
}
Then we use the following funtion to create an hidden DOM element to test out CSS properties within:
function test_css(element_name, element_classes, expected_styles, rsrc_name, callback) {
var elem = document.createElement(element_name);
elem.style.display = 'none';
for (var i = 0; i < element_classes.length; i++){
elem.className = elem.className + " " + element_classes[i];
}
document.body.appendChild(elem);
var handle = -1;
var try_count = 0;
var test_function = function(){
var match = true;
try_count++;
for (var key in expected_styles){
console.log("[CSS loader] testing " + rsrc_name + " css : " + key + " = '" + expected_styles[key] + "', actual = '" + get_style_of_element(elem, key) + "'");
if (get_style_of_element(elem, key) === expected_styles[key]) match = match && true;
else {
console.error("[CSS loader] css " + rsrc_name + " test failed (not ready yet ?) : " + key + " = " + expected_styles[key] + ", actual = " + get_style_of_element(elem, key) );
match = false;
}
}
if (match === true || try_count >= 3){
if (handle >= 0) window.clearTimeout(handle);
document.body.removeChild(elem);
if (!match) console.error("[CSS loader] giving up on css " + rsrc_name + "..." );
else console.log("[CSS loader] css " + rsrc_name + " load success !" );
callback(rsrc_name, match);
}
return match;
}
if(! test_function()){
console.info("" + rsrc_name + " css test failed, programming a retry...");
handle = window.setInterval(test_function, 100);
}
}
Now we can reliably know, within Modernizr, if our CSS is ready to go:
Modernizr.load([
{
load: { 'bootstrap-css': 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css' },
callback: function (url, result, key){
//THIS IS OUR TEST USING THE ABOVE FUNCTIONS
test_css('span', ['span1'], {'width': '60px'}, function(match){
if (match){
console.log('CDN Resource Loaded!');
} else {
console.log('Fallback to local resource!')
}
}
}
}
]);