Resources » GlobalStylesheet jQuery Plugin
Enables CSS modification that uses a 'global' stylesheet, rather than inline CSS. This is particularly handy for modifying CSS styles that you want to remain persistent until a page is refreshed again.
Use the 'globalcss' function in the same way as the jQuery 'css' function. Eg:
$("some selector").globalcss("style","value");
Use the globalsetylesheet.print() function to return a string of the global stylesheet
$("p").globalcss("color","red");
This plugin creates a stylesheet and appends it to the head of the page. The document.styleSheets object is used to reference the new stylesheet, and add styles to it. References to new styles are cached in an associative array, which uses the CSS selector query as the key, to help with performance (rather than searching through the document.styleSheets object).
This plugin requires jquery 1.3.1 or above, unless I figure out how to access the jQuery.selector variable in older versions.
jquery.globalstylesheet.js - version 0.1
I havn't done much cross-browser testing yet. Feel free to test it out and let me know of issues that arise.
Thanks to hunlock.com for a blog that helped me understand how use the document.styleSheets object.
Nice plug-in. It saved me the trouble of writing a cross browser script from scratch. Light weight and easy to use. Thanks, Burnbright!
Posted by Tom Nicolosi, 11/06/2010 10:01am (2 months ago)
I have aldo found a video tutorial on how to make a jquery plugin, hope it will also be helpful. Everything seems to be quite clear there.
Posted by globalstylesheet-jquery-plugin, 05/06/2010 1:34am (2 months ago)
I was playing a bit with this code, and some other similar code ended up extending it quite a bit for the site above, which I'm actively developing...
This lets you do fun things like this:
CSS.cloneRules('.ui-state-default,.ui-corner-all',
'form input,form select,form button');
CSS.cloneRules('.ui-state-hover',
'form input:hover,form select:hover,form button:hover');
CSS.cloneRules('.ui-state-focus',
'form input:focus,form select:focus,form button:focus');
CSS.cloneRules('.ui-state-active',
'form input:active,form select:active,form button:active');
CSS.cloneRules('.ui-priority-primary',
'form button,form button:hover,form button:focus,form button:active');
with jQuery UI Themeroller styles...
/*
* Implement some helper functions for strings.
*/
String.prototype.toCamelCase = function() {
return this.toLowerCase().replace(/\-([a-z])/g, function(x, y) {
return y.toUpperCase();
});
};
String.prototype.toProperty = function() {
return this.replace(/([A-Z])/g,'-$1').toLowerCase();
};
String.prototype.trim = function() {
return this.replace(/^\s+|\s+$/g,'');
};
/*
* A split function which trims and cleans the string first, so no empty values
* are returned, and we don't have to trim the results.
*/
String.prototype.trimsplit = function(fence) {
var re1 = new RegExp('\\s*'+fence+'\\s*','g'); // No Internal Space
var re2 = new RegExp('('+fence+')+','g'); // Empty
var re3 = new RegExp('^'+fence+'|'+fence+'$','g'); // Leading and Trailing
var s = this.trim().replace(re1,fence).replace(re2,fence).replace(re3,'');
return (s === '' ? [] : s.split(fence));
};
/*
* CSS Cloning.
*/
/*
* Based on:
* Global Stylesheet jQuery Plugin
* Version: 0.1
*
* Copyright (c) 2009 Jeremy Shipman (http://www.burnbright.co.nz)
*
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*/
var CSS = new function CascadingStyleSheets() {
if (!document.styleSheets) {
alert('document.Stylesheets not found');
return false;
}
// We attach to the first non-linked style sheet, and only process styles
// up to that point. We also need to fix up urls in style sheets which
// are linked at a different level to us, so store some urls for that.
var sheet, baseurl = false, docurl = document.URL.replace(/\?.*/g,'');
for (var i = 0; i < document.styleSheets.length; ++i) {
if (!document.styleSheets.href
|| document.styleSheets.href === '') {
sheet = document.styleSheets;
}
}
// We didn't find a non-linked style sheet, so create one.
if (!sheet) {
var cssNode = document.createElement('style');
cssNode.type = 'text/css';
cssNode.rel = 'stylesheet';
document.getElementsByTagName('head')[0].appendChild(cssNode);
sheet = document.styleSheets[document.styleSheets.length-1];
}
// The rules cache consists is an array of objects. The array key is a
// single selector and the value is the declaration. The selector is a
// string, and the declaration is a object, with properties named after the
// CSS properties (but camelCase), and the values as strings.
this.rulesCache = [];
function extendCache(cache, cssText) {
cssText = cssText.replace(/[\r\n]/g,'');
var tmp = /^(.*)\{(.*)\}/.exec(cssText);
// Ensure selectors are properly spaced
var sel = tmp[1].toLowerCase().replace(/\s+/g,' ')
.replace(/\s?([>+~])\s?/g,' $1 ').trimsplit(',');
var dec = tmp[2].trimsplit(';');
var o = {}, i, p;
for (i in dec) {
// Opera expands URLs, so just remove our url.
if (dec.match('://')) {
dec = dec.replace(docurl,'');
} else if (baseurl && dec.match('url')) {
dec = dec.replace('url(','url('+baseurl);
}
var t = /^([^:]*)[:](.*)/.exec(dec);
if (t[1] && (t[1] = t[1].trim()) !== ''
&& t[2] && (t[2] = t[2].trim()) !== '') {
o[t[1].toCamelCase()] = t[2];
}
}
if (o === {}) {
return;
}
for (i in sel) {
if (!cache[sel]) {
cache[sel] = {};
}
for (p in o) {
cache[sel][p] = o[p];
}
}
}
// This sets up the CSS cache, since we don't want to have to walk the
// style sheets all the time for cloning, etc. Since we are going to add
// our styles to the first local style-sheet, we only cache up to and
// including that sheet, which means any later sheets are never affected.
this.fillCache = function() {
this.rulesCache = [];
for (var i = 0; i < document.styleSheets.length; ++i) {
var s = document.styleSheets;
baseurl = false;
if (s.href && s.href.lastIndexOf('/') >= 0) {
baseurl = s.href.substr(0,s.href.lastIndexOf('/')+1).trim()
.replace(docurl,'').replace(/^[\/]?/g,'');
}
var rules = (s.cssRules ? s.cssRules : s.rules);
for (var j = 0; j < rules.length; ++j) {
// XXX: What about imports...
if (rules[j].type && rules[j].type !== 1) {
continue; // !CSSRule.STYLE_RULE
}
if (rules[j].cssText) {
extendCache(this.rulesCache,rules[j].cssText);
} else {
extendCache(this.rulesCache,rules[j].selectorText
+'{'+rules[j].style.cssText+'}');
}
}
if (s === sheet) {
break; // Don't process stylesheets after this.
}
}
};
// Build the cache on construction.
this.fillCache();
// Inserts a CSS rule
this.insertRule = function(selector, cssText) {
if (!selector || !cssText) {
return;
}
if (selector instanceof Array) {
selector = selector.join(', ');
}
try {
if (sheet.insertRule) {
sheet.insertRule(selector+'{'+cssText+'}',
sheet.cssRules.length);
} else {
selector = selector.trimsplit(',');
for (var i = 0; i < selector.length; ++i) {
sheet.addRule(selector,cssText);
}
}
extendCache(this.rulesCache,selector+'{'+cssText+'}');
} catch(e) {}
};
// Insert a bunch of rules, which must be an array of strings.
this.insertRules = function(rules) {
for (var i = 0; rules && rules.length && i < rules.length; ++i) {
var tmp = /^(.*)\{(.*)\}/.exec(String(rules)
.replace(/[\r\n]/g,''));
this.insertRule(tmp[1].trim(),tmp[2].trim());
}
};
// Predefined selector match and replace functions, which can be used as
// examples to create your own...
//
// Exact match only...
this.SELECTOR_EXACT = function(rs, sel, clone) {
clone = clone || sel;
return (rs === sel ? clone : false);
};
// Match start of selector only...
this.SELECTOR_START = function(rs, sel, clone) {
clone = clone || sel;
return ((rs+' ').indexOf(sel+' ') === 0
? clone + rs.substr(sel.length) : false);
};
// Match any complete part of the selector...
this.SELECTOR_PART = function(rs, sel, clone) {
clone = clone || sel;
return ((' '+rs+' ').indexOf(' '+sel+' ') >= 0
? (' '+rs+' ').replace(' '+sel+' ',' '+clone+' ').trim() : false);
};
// Match anywhere in the selector (inlcuding sub-strings)
this.SELECTOR_ALL = function(rs, sel, clone) {
clone = clone || sel;
return (rs.indexOf(sel) >= 0 ? rs.replace(sel,clone) : false);
};
// Builds a filter function. The filter function takes an object with
// properties as keys (in camelCase) and declarations as values. It must
// return a similar obect, with undesired properties filtered out or
// declarations adjusted as needed.
//
// The default filter uses a regexp to match properties. If it is passed
// a string (or array of strings) it builds a regexp to match based any
// string (e.g. 'background' would only copy background properties)
function buildFilter(filter) {
if (filter instanceof Array) {
filter = filter.join(',');
}
var re = (filter instanceof RegExp ? filter
: new RegExp(String(filter).trimsplit(',').join('|'), 'g'));
return function(rd) {
var rv = {};
for (var i in rd) {
if (i.toProperty().match(re)) {
rv = rd;
}
}
return rv;
};
}
// Get all rules matching the selector, after filtering. The return format
// is compatible with insertRules(). This is not that useful...
this.getRules = function(sel, match, filter) {
if (!sel) {
return;
}
if (sel instanceof Array) {
sel = sel.join(',');
}
sel = sel.toLowerCase().trimsplit(',');
if (!match || !(match instanceof Function)) {
match = this.SELECTOR_START;
}
if (filter && !(filter instanceof Function)) {
filter = buildFilter(filter);
}
var rs, rd, v, rules = [], i;
for (rs in this.rulesCache) {
rd = this.rulesCache[rs];
v = [];
for (var s in sel) {
if (!match(rs,sel)) {
continue;
}
if (!v.length) { // Lazy construction
if (filter) {
rd = filter(rd);
}
for (i in rd) {
v.push(i.toProperty()+':'+rd);
}
}
if (!v.length) {
break; // Empty result
}
rules.push(rs + '{' + v.join(';') + '}');
}
}
return rules;
};
// Clone all the rules which match the selector(s) to the selectors
// specified by clone, with matching and filtering. The rules are merged,
// so will look a little different, but should have the same result.
//
// XXX: At the moment specificity is not considered, so you should sort
// the selectors manually.
this.cloneRules = function(sel, clone, match, filter) {
if (!sel) {
return;
}
clone = clone || sel;
if (sel instanceof Array) {
sel = sel.join(',');
}
sel = sel.toLowerCase().trimsplit(',');
// XXX: sort sel by specificity...
if (clone instanceof Array) {
clone = clone.join(',');
}
clone = clone.toLowerCase().trimsplit(',');
if (!match || !(match instanceof Function)) {
match = this.SELECTOR_START;
}
if (filter && !(filter instanceof Function)) {
filter = buildFilter(filter);
}
var rs, rd, v, c, i, rules = [];
for (rs in this.rulesCache) {
rd = this.rulesCache[rs];
v = [];
for (var s in sel) {
if (!match(rs,sel)) {
continue;
}
if (!v.length) { // Lazy construction
if (filter) {
rd = filter(rd);
}
for (i in rd) {
v.push(i.toProperty()+':'+rd);
}
}
if (!v.length) {
break; // Empty result
}
c = [];
for (i in clone) {
c.push(match(rs,sel,clone));
}
rules.push(c.join(',')+'{'+v.join(';')+'}');
}
}
this.insertRules(rules);
};
// Delete all rules which match the selector(s)
this.deleteRules = function(sel, match) {
if (!sel) {
return;
}
if (sel instanceof Array) {
sel = sel.join(',');
}
sel = sel.toLowerCase().replace(/\s+/g,' ')
.replace(/\s?([>+~])\s?/g,' $1 ').trimsplit(',');
if (!match || !(match instanceof Function)) {
match = this.SELECTOR_START;
}
var rs, rd, i, j, rules, s, selectors, k;
for (i = 0; i < document.styleSheets.length; ++i) {
s = document.styleSheets;
rules = (s.cssRules ? s.cssRules : s.rules);
for (j = 0; j < rules.length; ++j) {
if (rules[j].type && rules[j].type !== 1) {
continue; // !CSSRule.STYLE_RULE
}
for (k = 0; k < sel.length; ++k) {
// Ensure selectors are properly spaced
selectors = rules[j].selectorText.toLowerCase()
.replace(/\s+/g,' ').replace(/\s?([>+~])\s?/g,' $1 ')
.trimsplit(',');
for (rs in selectors) {
if (!match(selectors[rs],sel[k])) {
continue;
}
try {
if (selectors.length === 1) {
if (s.deleteRule) {
s.deleteRule(j);
} else {
s.removeRule(j);
}
k = sel.length; // no more rule[j] processing
--j; // we removed rule[j]
} else {
selectors.splice(rs,1);
rd = '{'+rules[j].style.cssText+'}';
if (s.insertRule) {
s.insertRule(selectors.join(',')+rd,j);
s.deleteRule(j+1);
} else {
s.addRule(selectors.join(','),rd,j);
s.removeRule(j+1);
}
--k; // try again with sel[k]
}
if (this.rulesCache[selectors[rs]]) {
delete this.rulesCache[selectors[rs]];
}
} catch(e) {}
break;
}
}
}
if (s === sheet) {
break; // Don't process stylesheets after this.
}
}
};
// Gets the merged declarations for the selector
this.getMergedDeclarations = function(sel) {
if (!sel) {
return;
}
if (sel instanceof Array) {
sel = sel.join(',');
}
sel = sel.toLowerCase().replace(/\s+/g,' ')
.replace(/\s?([>+~])\s?/g,' $1 ').trimsplit(',');
// XXX: Sort by specificity...
var i, p, decl = {};
for (i in sel) {
if (this.rulesCache[sel]) {
for (p in this.rulesCache[sel]) {
decl[p] = this.rulesCache[sel][p];
}
}
}
return decl;
};
this.dumpCache = function() {
var rs, rd, v, i, div = document.createElement('div');
for (rs in this.rulesCache) {
rd = this.rulesCache[rs];
v = [];
for (i in rd) {
v.push(i.toProperty()+': '+rd);
}
div.appendChild(document.createElement('pre')).innerHTML =
rs + ' {\n\t' + v.join(';\n\t') + '\n}\n';
}
return div;
};
};
//hook new function into jQuery
jQuery.fn.extend({
insertRule: function(cssText) {
CSS.insertRule(this.selector,cssText);
},
insertDeclaration: function(key, value) {
CSS.insertRule(this.selector,key+':'+value+';');
},
deleteRules: function() {
CSS.deleteRules(this.selector,CSS.SELECTOR_EXACT);
}
});
Posted by Jeremy Lea, 11/06/2009 6:38am (1 year ago)