PDA

View Full Version : [UNKNOWN][3.x/2.x] Ext.util.CSS bombs when using @media



watrboy00
26 Aug 2009, 8:04 AM
Ext version tested:


Ext 2.x, 3.x



Adapter used:


ext



css used:


ext-all.css
custom css (see the code block below)




@media screen {
body {color:#404040;}
}

@media print {
body {color:#000;}
}Browser versions tested against:


IE6
Chrome
FF2/3
Safari 4
Opera



Operating System:


WinXP Pro SP3



Description:


Ext.util.CSS.cacheStyleSheet bombs when using a style sheet that sepearates rules based on media type. It fails because when parsing the cssRules or rules it only goes down one level. Well in this case the first level is the @media declaration and not the cssRules or rules. It should properly check for a level deeper and if found process it. I've updated Ext.util.CSS.cacheStyleSheet and added Ext.util.CSS.setRules.


Test Case:


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"/>
<title>Ext.util.CSS Test Case</title>
<link rel="stylesheet" type="text/css" href="../../resources/css/ext-all.css" />
<style type="text/css">
@media screen {
body {color:#404040;}
}

@media print {
body {color:#000;}
}
</style>
<script type="text/javascript" src="../../adapter/ext/ext-base.js"></script>
<script type="text/javascript" src="../../ext-all.js"></script>
<script type="text/javascript">
Ext.onReady(function(){
console.log( Ext.util.CSS.getRule( 'body' , true ) );
});
</script>
</head>
<body></body>
</html>The result that was expected:

A CSSRule for 'body' is to be returned to the console.


The result that occurs instead:

undefined is written to the console


Possible fix:


See below... (Changes in red)




/**
* @class Ext.util.CSS
* Utility class for manipulating CSS rules
* @singleton
*/
Ext.util.CSS = function(){
var rules = null;
var doc = document;

var camelRe = /(-[a-z])/gi;
var camelFn = function(m, a){ return a.charAt(1).toUpperCase(); };

return {
/**
* Creates a stylesheet from a text blob of rules.
* These rules will be wrapped in a STYLE tag and appended to the HEAD of the document.
* @param {String} cssText The text containing the css rules
* @param {String} id An id to add to the stylesheet for later removal
* @return {StyleSheet}
*/
createStyleSheet : function(cssText, id){
var ss;
var head = doc.getElementsByTagName("head")[0];
var rules = doc.createElement("style");
rules.setAttribute("type", "text/css");
if(id){
rules.setAttribute("id", id);
}
if(Ext.isIE){
head.appendChild(rules);
ss = rules.styleSheet;
ss.cssText = cssText;
}else{
try{
rules.appendChild(doc.createTextNode(cssText));
}catch(e){
rules.cssText = cssText;
}
head.appendChild(rules);
ss = rules.styleSheet ? rules.styleSheet : (rules.sheet || doc.styleSheets[doc.styleSheets.length-1]);
}
this.cacheStyleSheet(ss);
return ss;
},

/**
* Removes a style or link tag by id
* @param {String} id The id of the tag
*/
removeStyleSheet : function(id){
var existing = doc.getElementById(id);
if(existing){
existing.parentNode.removeChild(existing);
}
},

/**
* Dynamically swaps an existing stylesheet reference for a new one
* @param {String} id The id of an existing link tag to remove
* @param {String} url The href of the new stylesheet to include
*/
swapStyleSheet : function(id, url){
this.removeStyleSheet(id);
var ss = doc.createElement("link");
ss.setAttribute("rel", "stylesheet");
ss.setAttribute("type", "text/css");
ss.setAttribute("id", id);
ss.setAttribute("href", url);
doc.getElementsByTagName("head")[0].appendChild(ss);
},

/**
* Refresh the rule cache if you have dynamically added stylesheets
* @return {Object} An object (hash) of rules indexed by selector
*/
refreshCache : function(){
return this.getRules(true);
},

// private
cacheStyleSheet : function(ss){
if(!rules){
rules = {};
}
try{// try catch for cross domain access issue
var ssRules = ss.cssRules || ss.rules;
this.setRules(ssRules);
}catch(e){}
},

// private
setRules : function(iRules) {
for(var j = iRules.length-1; j >= 0; --j){
var ssRules = iRules[j].cssRules || iRules[j].rules;
if(ssRules){
this.setRules(ssRules);
}else{
rules[iRules[j].selectorText.toLowerCase()] = iRules[j];
}
}
},

/**
* Gets all css rules for the document
* @param {Boolean} refreshCache true to refresh the internal cache
* @return {Object} An object (hash) of rules indexed by selector
*/
getRules : function(refreshCache){
if(rules === null || refreshCache){
rules = {};
var ds = doc.styleSheets;
for(var i =0, len = ds.length; i < len; i++){
try{
this.cacheStyleSheet(ds[i]);
}catch(e){}
}
}
return rules;
},

/**
* Gets an an individual CSS rule by selector(s)
* @param {String/Array} selector The CSS selector or an array of selectors to try. The first selector that is found is returned.
* @param {Boolean} refreshCache true to refresh the internal cache if you have recently updated any rules or added styles dynamically
* @return {CSSRule} The CSS rule or null if one is not found
*/
getRule : function(selector, refreshCache){
var rs = this.getRules(refreshCache);
if(!Ext.isArray(selector)){
return rs[selector.toLowerCase()];
}
for(var i = 0; i < selector.length; i++){
if(rs[selector[i]]){
return rs[selector[i].toLowerCase()];
}
}
return null;
},


/**
* Updates a rule property
* @param {String/Array} selector If it's an array it tries each selector until it finds one. Stops immediately once one is found.
* @param {String} property The css property
* @param {String} value The new value for the property
* @return {Boolean} true If a rule was found and updated
*/
updateRule : function(selector, property, value){
if(!Ext.isArray(selector)){
var rule = this.getRule(selector);
if(rule){
rule.style[property.replace(camelRe, camelFn)] = value;
return true;
}
}else{
for(var i = 0; i < selector.length; i++){
if(this.updateRule(selector[i], property, value)){
return true;
}
}
}
return false;
}
};
}();

watrboy00
1 Sep 2009, 12:03 PM
?

stever
1 Sep 2009, 2:34 PM
I haven't tried you fix, but thanks for putting up a report -- this has been a bother to me as well.

evant
1 Sep 2009, 7:18 PM
That method does work, however it always grabs the first rule, so if you switch the order of the CSS declarations around, you get the print stylesheet as opposed to the screen.

watrboy00
2 Sep 2009, 9:37 AM
Yes I believe that is true. I can take another look at it and already have some ideas around potential fixes. I will update the first post when I finish.

watrboy00
2 Sep 2009, 9:38 AM
I guess as a point of discussion...if there are more child CSSRules would we always want to search for a screen declaration versus having to somewhere specify it?

hendricd
3 Sep 2009, 9:03 AM
@et al - Here is a replacement for util.CSS that I'll be introducing to ManagedIframe shortly. It adds consideration for the @media (media="screen"), and @imports modifiers, during styleSheets cache-building.

Now you can do:


var CSS= Ext.util.CSS;
//find just 'screen' media rule
var r = CSS.getRule('.x-toolbar-ct', false, 'screen');
or
//find the first one available
var r = CSS.getRule(['print:.x-toolbar-ct', 'screen:.x-toolbar-ct', '.x-toolbar-ct'] );

/**
* @class Ext.util.CSS
* Utility class for manipulating CSS rules
* @singleton
*/
Ext.util.CSS = function(){
var rules = null;
var doc = document;

var camelRe = /(-[a-z])/gi;
var camelFn = function(m, a){ return a.charAt(1).toUpperCase(); };

return {


/**
* Sets the document context for the next CSS operation
* @param {Document} docum The target document context
*/
setDocument : function(docum){
if(docum) {
if(doc != docum){
//reset the cached Rules on document change
rules = null;
}
doc = docum;
}
},

/**
* Creates a stylesheet from a text blob of rules.
* These rules will be wrapped in a STYLE tag and appended to the HEAD of the document.
* @param {String} cssText The text containing the css rules
* @param {String} id An id to add to the stylesheet for later removal
* @return {StyleSheet}
*/
createStyleSheet : function(cssText, id){
var ss;
var head = doc.getElementsByTagName("head")[0];
var rules = doc.createElement("style");
rules.setAttribute("type", "text/css");
if(id){
rules.setAttribute("id", id);
}
if(Ext.isIE){
head.appendChild(rules);
ss = rules.styleSheet;
ss.cssText = cssText;
}else{
try{
rules.appendChild(doc.createTextNode(cssText));
}catch(e){
rules.cssText = cssText;
}
head.appendChild(rules);
ss = rules.styleSheet ? rules.styleSheet : (rules.sheet || doc.styleSheets[doc.styleSheets.length-1]);
}
this.cacheStyleSheet(ss);
return ss;
},

/**
* Removes a style or link tag by id
* @param {String} id The id of the tag
*/
removeStyleSheet : function(id){
var existing = doc.getElementById(id);
if(existing){
existing.parentNode.removeChild(existing);
}
},

/**
* Dynamically swaps an existing stylesheet reference for a new one
* @param {String} id The id of an existing link tag to remove
* @param {String} url The href of the new stylesheet to include
*/
swapStyleSheet : function(id, url){
this.removeStyleSheet(id);
var ss = doc.createElement("link");
ss.setAttribute("rel", "stylesheet");
ss.setAttribute("type", "text/css");
ss.setAttribute("id", id);
ss.setAttribute("href", url);
doc.getElementsByTagName("head")[0].appendChild(ss);
},

/**
* Refresh the rule cache if you have dynamically added stylesheets
* @return {Object} An object (hash) of rules indexed by selector
*/
refreshCache : function(){
return this.getRules(true);
},

// private
cacheStyleSheet : function(ss, media){
if(!rules){
rules = {};
}

try{// try catch for cross domain access issue

Ext.each(ss.cssRules || ss.rules || [],
function(rule){
this.hashRule(rule, ss, media);
}, this);

//IE @imports
Ext.each(ss.imports || [],
function(sheet){
sheet && this.cacheStyleSheet(sheet,this.resolveMedia([sheet, sheet.parentStyleSheet]));
}
,this);

}catch(e){}
},

// @private
hashRule : function(rule, sheet, mediaOverride){

var mediaSelector = mediaOverride || this.resolveMedia(rule);

//W3C @media
if( rule.cssRules || rule.rules){
this.cacheStyleSheet(rule, this.resolveMedia([rule, rule.parentRule ]));
}

//W3C @imports
if(rule.styleSheet){
this.cacheStyleSheet(rule.styleSheet, this.resolveMedia([rule, rule.ownerRule, rule.parentStyleSheet]));
}

rule.selectorText &&
Ext.each((mediaSelector || '').split(','),
function(media){
rules[((media ? media.trim() + ':' : '') + rule.selectorText).toLowerCase()] = rule;
});

},

/**
* @private
* @param {Object/Array} rule CSS Rule (or array of Rules/sheets) to evaluate media types.
* @return a comma-delimited string of media types.
*/
resolveMedia : function(rule){
var media;
Ext.each([].concat(rule),function(r){
if(r && r.media && r.media.length){
media = r.media;
return false;
}
});
return media ? (Ext.isIE ? String(media) : media.mediaText ) : '';
},
/**
* Gets all css rules for the document
* @param {Boolean} refreshCache true to refresh the internal cache
* @return {Object} An object (hash) of rules indexed by selector
*/
getRules : function(refreshCache){
if(rules === null || refreshCache){
rules = {};
var ds = doc.styleSheets;
for(var i =0, len = ds.length; i < len; i++){
try{
this.cacheStyleSheet(ds[i]);
}catch(e){}
}
}
return rules;
},

/**
* Gets an an individual CSS rule by selector(s)
* @param {String/Array} selector The CSS selector or an array of selectors to try. The first selector that is found is returned.
* @param {Boolean} refreshCache true to refresh the internal cache if you have recently updated any rules or added styles dynamically
* @param {String} mediaSelector Name of optional CSS media context (eg. print, screen)
* @return {CSSRule} The CSS rule or null if one is not found
*/
getRule : function(selector, refreshCache, mediaSelector){
var rs = this.getRules(refreshCache);

if(Ext.type(mediaSelector) == 'string'){
mediaSelector = mediaSelector.trim() + ':';
}else{
mediaSelector = '';
}

if(!Ext.isArray(selector)){
return rs[(mediaSelector + selector).toLowerCase()];
}
var select;
for(var i = 0; i < selector.length; i++){
select = (mediaSelector + selector[i]).toLowerCase();
if(rs[select]){
return rs[select];
}
}
return null;
},


/**
* Updates a rule property
* @param {String/Array} selector If it's an array it tries each selector until it finds one. Stops immediately once one is found.
* @param {String} property The css property
* @param {String} value The new value for the property
* @param {String} mediaSelector Name(s) of optional media contexts. Multiple may be specified, delimited by commas (eg. print,screen)
* @return {Boolean} true If a rule was found and updated
*/
updateRule : function(selector, property, value, mediaSelector){

Ext.each((mediaSelector || '').split(','), function(mediaSelect){
if(!Ext.isArray(selector)){
var rule = this.getRule(selector, false, mediaSelect);
if(rule){
rule.style[property.replace(camelRe, camelFn)] = value;
return true;
}
}else{
for(var i = 0; i < selector.length; i++){
if(this.updateRule(selector[i], property, value, mediaSelect)){
return true;
}
}
}
return false;
}, this);
}

};

watrboy00
10 Sep 2009, 7:05 AM
Evan - What about Doug's version?