Skip to content

Commit

Permalink
v3.0.0
Browse files Browse the repository at this point in the history
- code tests
- transform functions
- cors handling
- code refactored
- module|| head include available
- MIT licence
- config Object
  • Loading branch information
colxi committed Jul 3, 2019
1 parent 70a3799 commit ff1cba7
Show file tree
Hide file tree
Showing 6 changed files with 393 additions and 296 deletions.
70 changes: 54 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ Demo : See it in action [here](https://colxi.github.io/css-global-variables/exam

# Syntax:

> new CSSGlobalVariables( [ configObject ] )
```javascript
new CSSGlobalVariables( [ configObject ] )
```

#### Parameters:
A Config Object can be provided, to customize some internal behaviour. The provided object can set any of the following properties :
Expand All @@ -34,26 +35,29 @@ A user-provided transform-function, that processes the CSS Variable names (befor


#### Return value:
The CSSGlobalVariables() Constructor returns a Proxy Object containing a **live Collection** with the found CSS global variables, as properties.
The CSSGlobalVariables() Constructor returns a `Proxy Object` containing a **live Collection** with the found CSS global variables.


# Installation :
You can choose betwen any of the following available distribution channels :

You can choose betwen any of the following available options/distribution channels :

- GIT : Clone the repository locally using git ( or download the latest release [here](https://github.com/colxi/css-global-variables/releases/latest) )
```html
- **GIT** : Clone the repository locally using git ( or download the latest release [here](https://github.com/colxi/css-global-variables/releases/latest) )
```bash
$ git clone https://github.com/colxi/css-global-variables.git
```

- NPM : Install it using npm and import it.
- **NPM** : Install it using npm and import it.
```bash
$ npm install css-global-variables -s
```

- **CDN** : Include the script in your HTML header ( `window.CSSGlobalVariables` will be created ).
```html
<script src="https://colxi.info/css-global-variables/src/main.js"></script>
```

# Usage
The Constructor returns a Proxy Object. Any regular Object operation can be performed on it (except property deletion). In the following list, you will find examples of the the most common operations and interactions :
The `Constructor` returns a `Proxy Object` and any regular Object operation can be performed on it (except property deletion). In the following list, you will find examples of the the most common operations and interactions :

**Import and Construct** :
```javascript
Expand All @@ -80,17 +84,51 @@ console.log( cssVar['--myVariable'] );

**Enumeration** of all declared CSS3 global variables, through iteration :
```javascript
for(let v in cssVar){
if ( cssVar.hasOwnProperty(v) ) console.log(v);
for( let v in cssVar ){
console.log( v , '=', cssVar[v] );
}
```

# Normalize functions
`Normalize functions` (implemented by [@SebastianDuval](https://github.com/SebastianDuval) ) allow you to perform automatic transformations in the variable names, to make them more suitable for the javascript syntax, or to simply addapt them to your coding style and personal preferences.

In the following example a CSS variable declared using hyphens (`--my-css-variable`), can be accessed in Javascript using the widelly used camelCase style (`myCssVariable`), thanks to the `camelToHyphens` normalize function (and the native `autoprefixer`) :

CSS :
```html
<style>
:root{
--my-css-variable : 'red';
}
</style>
```
Javascript :
```javascript
let camelToHyphens = function(name){
return name.replace(/[A-Z]/g, m => "-" + m.toLowerCase() );
}
let cssVar = new CSSGlobalVariables( { normalize:camelToHyphens });

cssVar.myCssVariable = 'blue';
```

> Note : Styles which source is affected by a **Cross Origin Policy Restriction** will be ignored, and not included in the CSS Global Variables live Object

---
# DOM changes:

The library uses a DOM Mutation Observer to detect new inclusion or removals in the document. Thanks to this observer, new CSS variables are available automatically, when new styles are attached to the document.
# DOM changes tracking:

The library uses a DOM Mutation Observer to detect new inclusion in the document. Thanks to this observer, new CSS variables are available automatically, when new styles are attached to the document.


# CORS
CSSGlovalVariables will face limitations when trying to extract the CSS definitions of a remote stylesheet (except for same-origin urls). Restrictions applied by the browser, based in the Cross Origin Policy will block any access attempt.

In such scenario, a warning will be printed in console, and the affected style element will be flagged and ignored by the library.

The best practice, to prevent this restrictions is to insert the `crossorigin` attribute in the HTML element, as follows :

```html
<link rel="stylesheet" crossorigin="anonymous" href="https://www.a-remote-server/styles.css">
```

If the server is configured to allow CORS ( throught the **Access-Control-Allow-Origin** directive) the CORS restrictions should disapear.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"author": "colxi <[email protected]> (http://www.colxi.info)",
"version": "2.0.3",
"version": "3.0.0",
"keywords": [
"css global variables",
"css vars helper",
Expand Down
257 changes: 4 additions & 253 deletions src/css-global-variables.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,259 +8,10 @@
*
*/

// private ID counter
let __identifierCounter__ = 0;
import './main.js';

/**
*
* new CSSGlobalVariables() : Returns a Proxy containing all the found CSS Global Variables.
* Accepts a Configuration Object, with the following properties:
* filter : [String],
* autoprefix : [Boolean],
* normalize : [Function]
*
**/
const CSSGlobalVariables = function( configObj = {} ){

// Usage of 'new' keyword is mandatory
if( !new.target ) throw new Error('Calling CSSGlobalVariables constructor without "new" is forbidden');
let _temp = window.CSSGlobalVariables;

// __config__ : Object containing the instance configuration.
// Declare config properties and default values...
const __config__ = {
filter : false,
autoprefix : true,
normalize : false
};

// Validate Config Object type and property values types
if( typeof configObj !== 'object' ) throw new Error('CSSGlobalVariables constructor expects a config Object as first argument');

if( configObj.hasOwnProperty('normalize') && typeof configObj.normalize !== 'function' ){
throw new Error('Config property "normalize" must be a function');
}
if( configObj.hasOwnProperty('autoprefix') && typeof configObj.autoprefix !== 'boolean' ){
throw new Error('Config property "autoprefix" must be a boolean');
}
if( configObj.hasOwnProperty('filter') ){
if( typeof configObj.filter !== 'string' ) throw new Error('Config property "filter" must be a string');
else{
try{ document.querySelectorAll( configObj.filter ) }
catch(e){
throw new Error('Provided "filter" is an invalid selector ("'+configObj.filter+'")');
}
}
}

// Assign user provided config to config object
Object.assign(__config__, configObj);

// Generate and assign instance ID
__identifierCounter__++;
__config__.id = __identifierCounter__;


// __varsCache__ : Contains (internally) the CSS variables and values.
const __varsCache__ = {};

/**
*
* varsCacheProxy (Proxy Object) : Public Proxy object containing the CSS
* variables and their values. Provides bindeºd methods for live getting and
* setting, the variables values.
*
* @type {Proxy Object}
*
*/
const varsCacheProxy = new Proxy( __varsCache__ , {
get: function( target, name ){
// check if there is any new CSS declarations to be considered
// before returning any
//updateVarsCache();
name = normalizeVariableName( name );
return Reflect.get(target,name);
},
set: function(target, name, value){
//updateVarsCache();
name = normalizeVariableName( name );
value = String(value);
// set the variable value
document.documentElement.style.setProperty( name, value );
// update th cache object
return Reflect.set(target, name, value);
},
deleteProperty: function () {
/* not allowed */
//updateVarsCache();
return false;
},
has: function (target, name) {
//updateVarsCache();
name = normalizeVariableName( name );
return Reflect.has(target,name);
},
defineProperty: function (target, name, attr) {
//
// it only allows to set the value
//
//updateVarsCache();
name = normalizeVariableName( name );
if( typeof attr==='object' && attr.hasOwnProperty('value') ){
let value = String(attr.value);
// set the CSS variable value
document.documentElement.style.setProperty( name , value );
// update the cache
Reflect.set(target, name, value);
}
return target;
},
ownKeys: function (target) {
//updateVarsCache();
return Reflect.ownKeys(target);
},
getOwnPropertyDescriptor: function(target, name) {
//updateVarsCache();
name = normalizeVariableName( name );
return Reflect.getOwnPropertyDescriptor(target, name);
}
});

/**
*
* normalizeVariableName() Forces name to be a String, and attach the
* mandatory '--' prefix when autoprefixer is Enabled
*
* @param {[String]} name Name of thw requested variable
*
* @return {[String]}
*
*/
function normalizeVariableName( name = '' ){
name = String(name);
// if normalize was provided execute it
if( __config__.normalize ) name = __config__.normalize( name );

// If CSSVar name does not start with '--', prefix it, when __config__.autoprefix=true,
// or trigger an Error when not.
if( name.substring(0,2) !=='--' ){
if(__config__.autoprefix ) name = '--' + name;
else throw new Error('Invalid CSS Variable name. Name must start with "--" (autoprefix=false)');
}

return name;
}


/**
*
* updateVarsCache() : Updates the variables and values cache object. Inspects
* STYLE elements and attached stylesheets, ignoring those that have been
* previously checked. Finally checks the inline CSS variables declarations.
* Analized Elements will be Flagged wth an Htmlmattribute
*
* @return {[boolean]} Returns true
*
*/
function updateVarsCache(){
// iterate all document CSS files, and extract all the
// varibles set inside the :root selector
[].slice.call(document.styleSheets).reduce( (prev,_styleSheet)=>{
// if filters have been provided to constructor...
if( __config__.filter ){
// get all style type elements, and ignore current if not
// been selected by the provided filter
let s = document.querySelectorAll( __config__.filter );
let isSelected = false;
for( let i in s ) if( s.hasOwnProperty(i) && _styleSheet.ownerNode === s[i] ){
isSelected = true;
}
if(!isSelected) return;
}
// if Style element has been previously analized ignore it, if
// not mark element as analized to prevent future analysis
let ids = _styleSheet.ownerNode.getAttribute('css-global-vars-id');

if( String(ids).split(',').includes( String(__config__.id) ) ) return;
else{
// not cached yet!
let value = _styleSheet.ownerNode.getAttribute('css-global-vars-id');
// check if is null or empty (crossbrowser solution), and
// attach the new instance id
if( value === null || value === '' ) value = __config__.id;
else value += ',' + __config__.id;
// set the new value to the object
_styleSheet.ownerNode.setAttribute('css-global-vars-id', value);
}

// Use Try/Catch to detect Cross-Origin restrictions
let rules;
try{ rules = _styleSheet.rules || _styleSheet.cssRules }
catch(e){ console.warn( e ) }

// iterate each CSS rule (if found).
if (!rules) return;
else return prev + [].slice.call(rules).reduce( (prev, cssRule)=>{
// select only the :root entries
if ( cssRule.selectorText === ':root' ) {
let css = cssRule.cssText.split( '{' );
css = css[1].replace( '}' , '' ).split( ';' );
// iterate each :root CSS property
for (let i = 0; i < css.length; i++) {
let prop = css[i].split(':');
// if is a varIABLE property, insert in in cache
if (prop.length === 2 && prop[0].indexOf('--') === 1){
__varsCache__[ prop[0].trim() ] = prop[1].trim();
}
}
}
}, '');
}, '');
// After collecting all the variables definitions, check their computed
// values, consulting the :root element inline style definitions,
// and and assigning those values to the variables, in cache
for( let p in __varsCache__){
if( __varsCache__.hasOwnProperty(p) ){
__varsCache__[p] = window.getComputedStyle(document.documentElement,null).getPropertyValue(p).trim();
}
}
// done !
return true;
}


// Create a mutation observer. When new styles are attached to the DOM (Style or Link element)
// will pperform an update of the document CSS variables
var observer = new MutationObserver( mutations=>{
let update = false;
mutations.forEach( mutation=>{
if( mutation.type === 'childList' ){
for(let i=0;i<mutation.addedNodes.length;i++){
if( mutation.addedNodes[i].tagName === 'STYLE' || mutation.addedNodes[i].tagName === 'LINK' ) update= true;
}
for(let i=0;i<mutation.removedNodes.length;i++){
if( mutation.removedNodes[i].tagName === 'STYLE' || mutation.removedNodes[i].tagName === 'LINK' ) update= true;
}
}
});
if(update){
// update needs to be scheduled to garanted that the new styles
// are visible through the document.styleSheets API
setTimeout( updateVarsCache, 500 );
}
});
// Initialize the observer. Set the target and the config
observer.observe(document.documentElement, {
attributes: false,
childList: true,
characterData: true,
subtree: true
});

// analize the document style elements to generate
// the collection of css variables, and return the proxy object
updateVarsCache();
return varsCacheProxy;
};

export {CSSGlobalVariables};
delete window.CSSGlobalVariables;

export {_temp as CSSGlobalVariables};
Loading

0 comments on commit ff1cba7

Please sign in to comment.