Tuesday, March 8, 2011

Using Palettes with CSS

Context: Often times when working on a project, I find that there is a limited number of colors, or a palette, that we are going to be using. But in the CSS each color is specified independently. I would love to be able to say use "corporateGreen" or "corporateBrown" in my css rather then litter the entire css or skins with hex values. This will improve readability, consistency and maintenance.

So here is the solution that I have:

  1. Define a Palette Class based on Proxy that can translate names (String) to colors(uint)
  2. Have this Palette Class read in an embeded text file that defines the colors
  3. Pass the palettes to a stylesheetMixin class that parses the css to replace the names in the css with the hexdecimal values before applying the styles.

Palette.as
package com.squaredi.styles
{
 /**
  * 
  * @author Drew Shefman 
  * dshefman at squaredi dot com
  * 
  */
 import flash.utils.Dictionary;
 import flash.utils.Proxy;
 import flash.utils.flash_proxy;
 
 import mx.core.ByteArrayAsset;
 
 /**
  * Read name value pairs of color names from an "external file."
  * "external file" is quoted because to get it to sync with CSS we actually embed the external files.
  * Here is an example of the external file 
* 
  * _companyRed=0xFF0000
  * _companyGreen=0x00FF00
  * _companyBlue=0x0000FF
  * _columnGreen=_companyGreen
  * 
  * Note: Watch out for spaces in the file
*/
 dynamic public class Palette extends Proxy
 {
  public static var throwErrorsForMissingColors:Boolean = true;
  public static const MISSING_COLOR:int = -1
  
  private  var dict:Dictionary;
  public var paletteName:String;
  
  public function Palette(p_name:String)
  {
   dict = new Dictionary();
  }
  
  /**
   * Create an instance of the embedded text file, 
   * Process the file and split it into name value pairs
   * Save off the names into the palette
   **/ 
  public  function addColorsViaEmbededTextClass(theClass:Class):void
  {
   //Convert the embeded text file to a string
   var byteArray:ByteArrayAsset = ByteArrayAsset(new theClass());
   var str:String = byteArray.readUTFBytes(byteArray.length);
   
   //Clean up extra characters
   str = str.split(" ").join(""); //remove white spaces
   
   
   //Loop over name/value pairs
   var split:Array = str.split("\r\n");
   var obj:Object = this;
   var key:String;
   var value:String;
   var pair:Array;
   var pairStr:String;
   var len:int = split.length;
   for ( var i:int =0 ; i < len ; i++ )
   {
    pairStr = split[i];
    pair = pairStr.split("=");
    key = pair[0];
    value = pair[1];
    obj[key] = value;
   }
  }
  
  
  flash_proxy override function getProperty(name:*):*
  {
   if (isValid(name))
   {
    if (flash_proxy::hasProperty(name))
    {
     return Number(dict[name]);
    }
    else
    {
     if (throwErrorsForMissingColors)
     {
      throw new Error(name + " is an invalid color. Please look in the external colors file to figure out the name.");
     }
     else
     {
      return MISSING_COLOR;
     }
    }
   }
   return null;
  }
  
  flash_proxy override function hasProperty(name:*):Boolean
  {
   return dict[name] != null;
  }
  
  flash_proxy override function setProperty(name:*, value:*):void
  {
   
   //Make sure the name/values are not blank
   if (name=="" || value == "") {return;}
   
   //name comes in as a QName, so we need to getString to get the value out of it.
   name = name.toString();
   
   //Allow for colors to reference previously defined colors
   if (isValid(value)) { value = flash_proxy::getProperty(value)};
   
   //Verify that the name is correct and save it 
   if (isValid(name))
   {
    dict[name] = value;
   }
   else
   {
    throw new Error("Palette colors should start with a '_' to allow for easy distinction from other css variables");
   }
  }
  
  /**
   * Force that color variables start with and "_" 
   * */
  private function isValid(value:String):Boolean
  {
   if (value.indexOf("_") == 0)
   {
    return true;
   }
   
   return false;
  }
  
 }
}

StyleSheetMixin.as:
StyleSheeMixin.as
usage: http://stackoverflow.com/questions/2292127/how-to-have-constants-in-flex-css-files
// =================================================================
/* 
*  http://stackoverflow.com/questions/2292127/how-to-have-constants-in-flex-css-files** *  Copyright (c) 2010 viatropos http://www.viatropos.com/
 *  Lance Pollard
 *  lancejpollard at gmail dot com
 *  
 *  Permission is hereby granted, free of charge, to any person
 *  obtaining a copy of this software and associated documentation
 *  files (the "Software"), to deal in the Software without
 *  restriction, including without limitation the rights to use,
 *  copy, modify, merge, publish, distribute, sublicense, and/or sell
 *  copies of the Software, and to permit persons to whom the
 *  Software is furnished to do so, subject to the following
 *  conditions:
 * 
 *  The above copyright notice and this permission notice shall be
 *  included in all copies or substantial portions of the Software.
 * 
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 *  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 *  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 *  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 *  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 *  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 *  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 *  OTHER DEALINGS IN THE SOFTWARE.
 */
// =================================================================

package com.squaredi.styles
{
 import flash.display.Sprite;
 import flash.events.Event;
 import flash.utils.getDefinitionByName;
 
 import mx.core.IMXMLObject;
 import mx.core.Singleton;
 import mx.styles.CSSStyleDeclaration;
 import mx.styles.IStyleManager2;
 import mx.styles.StyleManager;
 
 public class StylesheetMixin implements IMXMLObject
 {
  private var _palettes:Array;
  /**
   *  Classes of static constants storing values for css
   */
  public function get palettes():Array
  {
   return _palettes;
  }
  public function set palettes(value:Array):void
  {
   _palettes = value;
  }
  
  public function StylesheetMixin()
  {
  }
  
  public function setStyles():void
  {
   // get all selectors in the application
   var styleManager:IStyleManager2 = getStyleManager();
   var selectors:Array = styleManager.selectors;
   var declaration:CSSStyleDeclaration;
   var i:int = 0;
   var n:int = selectors.length;
   for (i; i < n; i++)
   {
    declaration = styleManager.getStyleDeclaration(selectors[i]);
    // set palette properties to each declaration
    setProperties(declaration);
   }
  }
  
  protected function getStyleManager():IStyleManager2
  {
   var application:*;
   try {
    application = flash.utils.getDefinitionByName("mx.core::FlexGlobals")["topLevelApplication"];
    if (application)
     return application.styleManager as IStyleManager2;
   } catch (error:Error) {
    application = flash.utils.getDefinitionByName("mx.core::Application")["application"];
    if (application)
     return IStyleManager2(Singleton.getInstance("mx.styles::IStyleManager2"));
   } 
   return null;
  }
  
  protected function setProperties(declaration:CSSStyleDeclaration):void
  {
   var selector:Object = getDeclarationToken(declaration);
   var property:String;
   for (property in selector)
   {
    setProperty(declaration, property, selector[property]);
   }
  }
  
  public function getDeclarationToken(declaration:CSSStyleDeclaration):Object
  {
   var selector:Object = {factory:declaration.factory};
   // maybe your selector has a "factory" property which we should avoid?
   if (!(typeof(selector.factory) == "function") || selector.factory == null)
    return null;
   selector.factory();
   delete selector.factory;
   return selector;
  }
  
  public function setProperty(declaration:CSSStyleDeclaration, property:String, value:*):*
  {
   var paletteValue:*;
   var changed:Boolean = false;
   if (value is Array)
   {
    var i:int = 0;
    var n:int = value.length;
    for (i; i < n; i++)
    {
     paletteValue = getPaletteItem(value[i]);
     if (paletteValue)
     {
      changed = true;
      value[i] = paletteValue;
     } 
    }
    
   }
   else if (value is String)
   {
    paletteValue = getPaletteItem(value);
    if (paletteValue)
    {
     value = paletteValue;
     changed = true;
    }
   }
   if (changed)
   {
    declaration.setStyle(property, value);
   }
  }
  
  public function getPaletteItem(targetId:String):*
  {
   var i:int = 0;
   var n:int = palettes.length;
   var PaletteClass:Object;
   for (i; i < n; i++)
   {
    PaletteClass = palettes[i];
    if (PaletteClass[targetId])
     return PaletteClass[targetId];
   }
   return null;
  }
  
  private var timer:Sprite = new Sprite();
  // have to wait a frame for styles to be initialized
  public function initialized(document:Object, id:String):void
  {
   var handler:Function = function(event:Event):void
   {
    timer.removeEventListener(Event.ENTER_FRAME, handler);
    timer = null;
    setStyles();
   }
   timer.addEventListener(Event.ENTER_FRAME, handler);
  }
 } 
}
So then in your Application, the way that you would use it would be:
Application.mxml
<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
      xmlns:s="library://ns.adobe.com/flex/spark" 
      xmlns:mx="library://ns.adobe.com/flex/mx" minWidth="955" minHeight="600"
      xmlns:styles="com.squaredi.styles.*"
      
      initialize="onInitialize(event)" 
      >
 <fx:Script>
  <![CDATA[
   import com.squaredi.styles.Palette;
   
   import mx.events.FlexEvent;
   [Embed(source="examples/styles/palette.txt", mimeType="application/octet-stream")]
   public  var paletteClass:Class
   
   [Bindable] private var allPalettes:Array
   
   protected function onInitialize(event:FlexEvent):void
   {
    var pal:Palette = new Palette("main");
    pal.addColorsViaEmbededTextClass(paletteClass);
    
    allPalettes = new Array(pal);
    
   }


  ]]>
 </fx:Script>
 
 <fx:Style>
  @namespace s "library://ns.adobe.com/flex/spark";
  @namespace mx "library://ns.adobe.com/flex/mx";
  
  
  
  .example
  {
   backgroundColor: _corpRed;
   backgroundAlpha:1.0;
   
  }
  </fx:Style>
 
 <fx:Declarations>
  <!-- Place non-visual elements (e.g., services, value objects) here -->
  <styles:StylesheetMixin palettes="{allPalettes}" />
 </fx:Declarations>
 <s:SkinnableContainer styleName="example" width="200" height="200" >
  
  <s:Label text="Hello world" />
 </s:SkinnableContainer>
</s:Application>

and your embeded text file would look like:
_corpRed=0xFF0000

No comments:

Post a Comment