Category Archives: design

Dynamically remove blank cells from a column in Excel

This is another Xcelsius issue i had — but the solution is usable in any excel spreadsheet.

I wanted to create a fake drop down (custom image w/ little arrow like most web pages have now + push button + list box) that i could set up with dynamic options — dynamic meaning different options based on different states of the dashboard, not adding or removing new options at runtime.  The problem was that i needed to link to a range in the Excel spreadsheet that gave me the currently available options and did not have blank spaces between them.  I hunted on the internet and found an example, but it used functions that are not available in the Xcelsius-enabled list of Excel functions.

So I edited it to be Xcelsius-friendly and here it is (click on this pic to see a larger version):

Table layout for dynamically removing blank cells

Basic table layout for setup

  • Column 1 (H): a row number that is copied across to column 4 when the option text is marked as “available” in column 3.  these are hardcoded numbers.  Xcelsius doesn’t support the ROW function.
  • Column 2 (I): The table of data.  In my case it doesn’t have spaces, but effectively does when i have certain options marked as “unavailable”.
  • Column 3 (J): A formula that is unique for each row and determines if THAT row should be available.  This checks the status of different things in the Xcelsius file (e.g., the dynamic visibility of other components in order to determine what tab on on, etc) and determines when this option should be available in my faked-out combo box implementation.  If you have data that has spaces in it that you are removing, this row can check for that or (even better) just combined with column 4.
  • Column 4 (K): A filter.  If this row is “available”, then this cell is set equal to the row number in column 1.
  • Column 5 (L): blank… i left it there for spacing, or maybe for adding something later… i don’t know.  I can’t remember.  but it is blank.
  • Column 6 (M): The magic happens here… Keep reading

The function in Column 6 is this, the column letters are defined in the column definitions above (i.e., column 1 is H) and the first row of that table was at row 30:

=IF(ISERROR(SMALL($K$30:$K$49,H30)),"",INDEX($I$30:$I$49,SMALL($K$30:$K$49,H30)))

To break this down:

  1. ISERROR(SMALL($K$30:$K$49,H30)),””…: The first part here checks if there is a next smallest value in column 4.  if there is, it continues to the INDEX function, otherwise it leaves the cell in column 6 empty.  Column K is the sparsely populated filter column (aka column 4), and the “n” in the “nth next smallest value” comes from column H (the rownum).
  2. INDEX($I$30:$I$49,SMALL($K$30:$K$49,H30)):  Since we know an “nth next smallest value” exists in the filter column, this pulls the “option text” in column 2 (i.e., the table data) corresponding to that row number.  the row number starts at 1 for the first row, so it is equal to the “index” of that row when using the INDEX function.
Here is a screen shot to help you line up the formula to the columns.  If you setup the first cell in column 6 with the same “$” that i use for static cells, then a copy-down will fill in that whole column correctly:
Options table with formula 1st row

Showing the super awesome formula in the last column

It is actually pretty simple and useful.  I simplified it as i wrote it here because i found a function that I was using that was redundant, but in case it solved some other issue that i don’t have and can’t think of, here is it:

=IF(ISERROR(SMALL($K$30:$K$49,H30)),"",INDEX($I$30:$I$49,MATCH(SMALL($K$30:$K$49,H30),$K$30:$K$49,0)))

.   The MATCH at the end there is redundant.  In my example, the relative index is also equal to the “nth next smallest value” in the filter column — so i’m doing a MATCH using the SMALL function to get the same number the SMALL function already returns.  It is useful to write these posts and fix my own code!

I’m not maintaining this blog here so commenting is closed… Please see this same post on my new site to ask questions:http://mpickell.com/blog/2012/03/05/dynamically-remove-blank-cells-from-a-column-in-excel/

Advertisements

HTML5 Canvas: scrolling background and selecting/dragging shapes

I have been wanting to do this for a while and just had a chance to mess around with it this week.  I also had more motivation after spending Saturday in an “Designing (for) Interactions” workshop by Dan Mall, and hosted by RefreshPGH.  The key concepts were HTML5, CSS3, some jQuery, and generally getting some good experience on how people in the real world design web sites.  ( I really liked the boilerplate website, font squirrel, and of course A List Apart articles! )

Anyway, the thing I have been wanting to mess around with is the canvas.  I have recently been working on some mobile application prototypes and I am getting familiar with some of the components that exist there.  I wanted to see if I could put together a canvas demo to replicate a ScrollView, as well as being able to throw stuff in and manipulate it.  So here’s my example,with a little explanation.  It is a little hacked together because I put it together pretty quickly, so don’t grade on neatness and optimization —  many of the functions and comments are still very much what Simon Sarris wrote and I pieced things in around his code.  There are also bits and pieces of example code used from multiple places, like here.

The Details…

I started out with the code that Simon Sarris wrote for selectable shapes in a canvas.  After looking and understanding what he was doing, I refactored the code to be object oriented so it was more expandable.  My Goal: Understand the code well enough that I can edit it, make it expandable, and experiment with creating the “ScrollView” effect that I see on mobile devices.

Here is the canvas tag I used:

<canvas id="canvas_clickAndDragOO" width="400" height="300" style="border: 1px black solid;">
   This text is displayed if your browser does not support HTML5 Canvas.
</canvas>

The manipulation of the shapes is exactly how Simon wrote it, so check out his page for that explanation.  The main thing I added was the scrollable content.

Key Points:

  • I have two main things in the javascript:  a Canvas class and Shapes.  The shapes are pretty basic, so the Canvas class is the most interesting.  It maintains two canvas elements: one for the content and one for the view.  The content canvas is never added to the view and is only maintained behind the scenes as a javascript variable.  Everything is written to that content canvas and the context drawImage method is used to send a section of it to the viewable canvas.
  • To make the view ‘scroll,’ an X and Y value are maintained to specify the coordinates of the top left corner of the viewable canvas on the content canvas.  When the background of the canvas is clicked and dragged, this X and Y value are updated.  The draw method simply reacts to these new values and sends an new section of the content canvas out to the viewable canvas.  The viewable canvas is simply a window to a section of the content canvas.

The “content canvas” (hidden) is automatically created twice the size of the “viewable canvas” for this demo, but this and other things are configurable if you look at the Canvas object in the code.  Just include this in a webpage via a script tag and it should work with the canvas above.  (UPDATE: I pasted this here so it could be read and copied easier)

UPDATE 2:  This code doesn’t work on < IE8 using the excanvas.js library because that library does not support the getImageData function!  I would have to find a way around that in order for it to be backwards compatible.  Also, older IE browsers do not support the pageX and pageY properties on the mouse events. This issue can be solved quickly by looking here (search the page for “pageX”).

UPDATE 3: I cannot run a demo on this wordpress site, so here is a link to a zip file demo.  Unzip it and open index.html in your browser. This is on my new site … same blog, but i’m yet to redirect everything there…

/*
 * My object oriented version of click and drag
 *
 */

function Canvas( data ) {
  data = data || {};
  var self = this;

  this.canvasID = data.canvasID || 'canvas';
  this.viewableCanvas = document.getElementById(this.canvasID);
  this.vctx = data.canvasContext || this.viewableCanvas.getContext('2d');
  // set our events. Up and down are for dragging,
  // double click is for making new boxes
  this.viewableCanvas.onmousedown = function(e){ Canvas.prototype.mouseDownAction.call( self, e ) };
  this.viewableCanvas.onmouseup = function(e){ Canvas.prototype.mouseUpAction.call( self, e ) };
  this.viewableCanvas.onmouseout = function(e){ Canvas.prototype.mouseUpAction.call( self, e ) };
  this.viewableCanvas.ondblclick = function(e){ Canvas.prototype.mouseDblClickAction.call( self, e ) };

  this.isValid = Canvas.IS_INVALID;

  this.canvasContentObjectsList = [];

  // we use a content canvas to draw individual shapes.  This is larger than the viewable canvas.  
  // only a section of this is passed to the viewable canvas
  var contentCanvasHeight = data.contentCanvasHeight || this.viewableCanvas.height * 2;
  if (contentCanvasHeight < this.viewableCanvas.height) { contentCanvasHeight = this.viewableCanvas.height }
  var contentCanvasWidth = data.contentCanvasWidth || this.viewableCanvas.width * 2;
  if (contentCanvasWidth < this.viewableCanvas.width) { contentCanvasWidth = this.viewableCanvas.width }
  this.contentCanvas = document.createElement('canvas');
  this.contentCanvas.height = contentCanvasHeight;
  this.contentCanvas.width = contentCanvasWidth;
  this.cctx = this.contentCanvas.getContext('2d'); // content context
  // These X and Y values are the top left corner of the "viewable window" of this content canvas
  this.ccViewX = Math.max( (contentCanvasWidth/2) - (this.viewableCanvas.width/2), 0 );
  this.ccViewY = Math.max( (contentCanvasHeight/2) - (this.viewableCanvas.height/2), 0 );

  // Max values for the ccView X and Y vals
  this.ccViewXMax = this.cctx.canvas.width - this.vctx.canvas.width;
  this.ccViewYMax = this.cctx.canvas.height - this.vctx.canvas.height;

  this.isContentObjectDragAction = false;
  this.isContentCanvasDragAction = false;

  // the X and Y coordinate for when the mousedown event is triggered
  this.contentCanvasDragStartX = 0;
  this.contentCanvasDragStartY = 0;

    //fixes a problem where double clicking causes text to get selected on the canvas
  this.viewableCanvas.onselectstart = function () { return false; }

  // fixes mouse co-ordinate problems when there's a border or padding
  // see getMouse for more detail
  this.stylePaddingLeft = 0;
  this.stylePaddingTop = 0;
  this.styleBorderLeft = 0;
  this.styleBorderTop = 0;
  if (document.defaultView && document.defaultView.getComputedStyle) {
    this.stylePaddingLeft = parseInt(document.defaultView.getComputedStyle(this.viewableCanvas, null)['paddingLeft'], 10)      || 0;
    this.stylePaddingTop  = parseInt(document.defaultView.getComputedStyle(this.viewableCanvas, null)['paddingTop'], 10)       || 0;
    this.styleBorderLeft  = parseInt(document.defaultView.getComputedStyle(this.viewableCanvas, null)['borderLeftWidth'], 10)  || 0;
    this.styleBorderTop   = parseInt(document.defaultView.getComputedStyle(this.viewableCanvas, null)['borderTopWidth'], 10)   || 0;
  }

// The selection color and width. Right now we have a red selection with a small width
  this.selectedColor = data.selectedColor || '#CC0000';
  this.selectedWidth = data.selectedwidth || 2;

  // since we can drag from anywhere in a node
  // instead of just its x/y corner, we need to save
  // the offset of the mouse when we start dragging.
  this.offsetx = this.ccViewX;
  this.offsety = this.ccViewY;

  this.currentlySelectedContentObject = null;

    // make draw() fire every INTERVAL milliseconds
  setInterval(function() { Canvas.prototype.draw.call( self ) }, 1000/(data.interval?data.interval:40) );
}
// static members
Canvas.IS_VALID = true;
Canvas.IS_INVALID = false;

// create a random number between two ints, inclusive
Canvas.randomFromTo = function( from, to ) {
  return Math.floor(Math.random() * (to - from + 1) + from);
}

//wipes the canvas context
Canvas.prototype.clear = function( ctx ) {
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}

// While draw is called as often as the INTERVAL variable demands,
// It only ever does something if the canvas gets invalidated by our code
Canvas.prototype.draw = function( ) {
  if ( !this.isValid && this.cctx ) {
    //Canvas.prototype.clear.call( this, ctx );
    this.clear( this.cctx );

    // Add stuff you want drawn in the background all the time here
    //Draw a grid to assist in seeing the background scroll
    this.drawGrid();

    // draw all boxes
    var l = this.canvasContentObjectsList.length;
    for (var i = 0; i < l; i++) {
      this.canvasContentObjectsList[i].drawshape({
        ctx: this.cctx
      });
    }

    // draw selection
    // right now this is just a stroke along the edge of the selected box
    var selectedObj = this.currentlySelectedContentObject;
    if (selectedObj != null) {
      selectedObj.highlightBorder({
        ctx: this.cctx,
        selectedColor: this.selectedColor,
        selectedWidth: this.selectedWidth
      });
    }

    // copy section of content canvas out to the viewable canvas
    try {
      this.clear( this.vctx );
      this.vctx.drawImage(
        this.cctx.canvas,
        this.ccViewX,
        this.ccViewY,
        this.viewableCanvas.width,
        this.viewableCanvas.height,
        0,
        0,
        this.viewableCanvas.width,
        this.viewableCanvas.height
      )
    } catch(e) {
      // Do nothing.  a draw error may occur because we are updating this canvas
      // before the cctx.canvas is done drawing.
    }

    // Add stuff you want drawn on top all the time here

    this.isValid = Canvas.IS_VALID;
  }
}

// draw grid lines, but only on the part visible
Canvas.prototype.drawGrid = function( data ) {

  // Draw gridlines to help with scrolling
  for (var x = (this.ccViewX + 0.5); x < (this.ccViewX + this.vctx.canvas.width); x += 40) {
    this.cctx.moveTo(x, 0);
    this.cctx.lineTo(x, this.ccViewY + this.vctx.canvas.height);
  }
  for (var y = (this.ccViewY + 0.5); y < (this.ccViewY + this.vctx.canvas.height); y += 40) {
    this.cctx.moveTo(0, y);
    this.cctx.lineTo(this.ccViewX + this.vctx.canvas.width, y);
  }
  this.cctx.strokeStyle = "#eee";
  this.cctx.stroke();
}

// Sets mx,my to the mouse position relative to the canvas
// unfortunately this can be tricky, we have to worry about padding and borders
Canvas.prototype.getMouseCoordinates = function(e) {
      var element = this.viewableCanvas, offsetX = 0, offsetY = 0;

      if (element.offsetParent) {
        do {
          offsetX += element.offsetLeft;
          offsetY += element.offsetTop;
        } while ((element = element.offsetParent));
      }

      // Add padding and border style widths to offset
      offsetX += this.stylePaddingLeft;
      offsetY += this.stylePaddingTop;

      offsetX += this.styleBorderLeft;
      offsetY += this.styleBorderTop;

      var mx = e.pageX - offsetX + this.ccViewX;
      var my = e.pageY - offsetY + this.ccViewY;

      // These are the mouse coordinates on the VIEWABLE CANVAS
      return {
        mouseX: mx,
        mouseY: my
      };
}

// Happens when the mouse is clicked in the canvas
Canvas.prototype.mouseDownAction = function(e){

  var self = this;
  var mouseMoveFunction = function(e){ Canvas.prototype.mouseMoveAction.call( self, e ) };

  var mouseCoords = this.getMouseCoordinates(e);
  //Canvas.prototype.clear.call( this, ctx );
  this.clear( this.cctx );
  var l = this.canvasContentObjectsList.length;
  for (var i = l-1; i >= 0; i--) {
    // draw shape onto ghost context
      this.canvasContentObjectsList[i].drawshape({
        ctx: this.cctx,
        fill: 'black'
      });

    // get image data at the mouse x,y pixel
    var imageData = this.cctx.getImageData(mouseCoords.mouseX, mouseCoords.mouseY, 1, 1);
    //var index = (mouseCoords.mouseX + mouseCoords.mouseY * imageData.width) * 4;

    // if the mouse pixel exists, select and break
    if (imageData.data[3] > 0) {
      this.currentlySelectedContentObject = this.canvasContentObjectsList[i];
      this.offsetx = mouseCoords.mouseX - this.currentlySelectedContentObject.x;
      this.offsety = mouseCoords.mouseY - this.currentlySelectedContentObject.y;
      this.currentlySelectedContentObject.x = mouseCoords.mouseX - this.offsetx;
      this.currentlySelectedContentObject.y = mouseCoords.mouseY - this.offsety;
      this.isContentObjectDragAction = true;
      this.viewableCanvas.onmousemove = mouseMoveFunction;
      this.isValid = Canvas.IS_INVALID;
      //Canvas.prototype.clear.call( this, ctx );
      this.clear( this.cctx );
      return;
    }
  }
  // Register a drag action for the whole canvas
  if ( !this.isContentCanvasDragAction ) {
    this.contentCanvasDragStartX = mouseCoords.mouseX;
    this.contentCanvasDragStartY = mouseCoords.mouseY;
    this.originalCCViewX = this.ccViewX;
    this.originalCCViewY = this.ccViewY;
  }
  this.isContentCanvasDragAction = true;
  this.viewableCanvas.onmousemove = mouseMoveFunction;
  // havent returned means we have selected nothing
  this.currentlySelectedContentObject = null;
  // clear the ghost canvas for next time
  //Canvas.prototype.clear.call( this, ctx );
  this.clear( this.cctx );
  // invalidate because we might need the selection border to disappear
  this.isValid = Canvas.IS_INVALID;
}

// Happens when the mouse is moving inside the canvas
Canvas.prototype.mouseMoveAction = function(e){
  var mouseCoords = null;

  if ( this.isContentObjectDragAction ){
    mouseCoords = this.getMouseCoordinates(e);

    this.currentlySelectedContentObject.x = mouseCoords.mouseX - this.offsetx;
    this.currentlySelectedContentObject.y = mouseCoords.mouseY - this.offsety;   

    // something is changing position so we better invalidate the canvas!
    this.isValid = Canvas.IS_INVALID;
  }

  if ( this.isContentCanvasDragAction ) {
    mouseCoords = this.getMouseCoordinates(e);

    xChange = (mouseCoords.mouseX - this.contentCanvasDragStartX);
    yChange = (mouseCoords.mouseY - this.contentCanvasDragStartY);

    // Must move 30 pixels before scrolling.
    if (Math.abs(xChange) > 30 || Math.abs(yChange) > 30) {
      var newX = this.originalCCViewX - xChange;
      newX = newX < 0 ? 0 : newX;
      newX = newX > this.ccViewXMax ? this.ccViewXMax : newX;
      this.ccViewX = newX;

      var newY = this.originalCCViewY - yChange;
      newY = newY < 0 ? 0 : newY;
      newY = newY > this.ccViewYMax ? this.ccViewYMax : newY;
      this.ccViewY = newY;

      this.isValid = Canvas.IS_INVALID;
    }
  }
}

Canvas.prototype.mouseUpAction = function(){
  this.isContentObjectDragAction = false;
  this.isContentCanvasDragAction = false;
  this.viewableCanvas.onmousemove = null;
}

Canvas.prototype.addContentObject = function( obj ) {
  // Verify that this is the correct object type so we have X and Y coordinates

  this.canvasContentObjectsList.push( obj );
  this.isValid = Canvas.IS_INVALID;
}

// adds a new node
Canvas.prototype.mouseDblClickAction = function(e) {
  var mouseCoords = this.getMouseCoordinates(e);
  var randomColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16);

  switch( Canvas.randomFromTo( 1, 3 ) ) {
    case 1:
      this.addContentObject( new Circle({
          x: mouseCoords.mouseX,
          y: mouseCoords.mouseY,
          r: Canvas.randomFromTo( 20, 50 ),
          fill: randomColor
        })
      );
      break;
    case 2:
      this.addContentObject( new Rectangle({
          x: mouseCoords.mouseX,
          y: mouseCoords.mouseY,
          w: Canvas.randomFromTo( 20, 100 ),
          h: Canvas.randomFromTo( 20, 100 ),
          fill: randomColor
        })
      );
      break;
    case 3:
    default:
      this.addContentObject( new Triangle({
          x: mouseCoords.mouseX,
          y: mouseCoords.mouseY,
          w: Canvas.randomFromTo( 20, 100 ),
          h: Canvas.randomFromTo( 20, 100 ),
          fill: randomColor
        })
      );
      break;
  }
}

/***********************************************************
 * Shape super class
 *
 ***********************************************************/

function Shape( data ) {
  data = data || {};

  this.shape = data.shape || 'Shape';
  this.shapeID = data.id || 'Shape';

  this.fill = data.fill || '#444444';

  this.x = data.x;
  this.y = data.y;
}

// Draws a single shape to a single context
// draw() will call this with the normal canvas
// myDown will call this with the ghost canvas
Shape.prototype.drawshape = function( data ) {
    if (!data) return;

  // fill can be overridded if passed in
  data.ctx.fillStyle = data.fill || this.fill;

  // subclass specific
}

Shape.prototype.highlightBorder = function( data ) {
  // Handle the basic, standard highlight things here.
  if (!data) return;

  data.ctx.strokeStyle = data.selectedColor;
  data.ctx.lineWidth = data.selectedWidth;
}

/***********************************************************
 * Rectangle super class
 *
 ***********************************************************/

// Create subclass and reassign constructor to self.
Rectangle.prototype = new Shape();
Rectangle.prototype.constructor = Rectangle;
function Rectangle( data ) {
  data = data || {};

  Shape.call( this, data );

  this.w = data.w;
  this.h = data.h;
}

// Draws a single shape to a single context
// draw() will call this with the normal canvas
// myDown will call this with the ghost canvas
Rectangle.prototype.drawshape = function( data ) {
  Shape.prototype.drawshape.call(this, data); 

  // We can skip the drawing of elements that have moved off the screen:
  if (this.x > data.ctx.canvas.width || this.y > data.ctx.canvas.height) return;
  if (this.x + this.w < 0 || this.y + this.h < 0) return;

  data.ctx.fillRect(this.x,this.y,this.w,this.h);
}

Rectangle.prototype.highlightBorder = function( data ) {
  Shape.prototype.highlightBorder.call(this, data);
  data.ctx.strokeRect(this.x,this.y,this.w,this.h);
}

/***********************************************************
 * Circle super class
 *
 ***********************************************************/

// Create subclass and reassign constructor to self.
Circle.prototype = new Shape();
Circle.prototype.constructor = Circle;
function Circle( data ) {
  data = data || {};

  Shape.call( this, data );

  this.r = data.r; //radius
}

// Draws a single shape to a single context
// draw() will call this with the normal canvas
// myDown will call this with the ghost canvas
Circle.prototype.drawshape = function( data ) {
  Shape.prototype.drawshape.call(this, data); 

  // We can skip the drawing of elements that have moved off the screen:
  if (this.x > data.ctx.canvas.width || this.y > data.ctx.canvas.height) return;
  if (this.x + this.r < 0 || this.y + this.r < 0) return;

  this.drawCircle( data );
}

Circle.prototype.highlightBorder = function( data ) {
  Shape.prototype.highlightBorder.call(this, data); 

  data['borderOnly'] = true;
  this.drawCircle( data );
}

Circle.prototype.drawCircle = function( data ) {
    // Draw a circle using the arc function.
  data.ctx.beginPath();

  // Arguments: x, y, radius, start angle, end angle, anticlockwise
  data.ctx.arc(this.x, this.y, this.r, 0, 360, false);
  if ( !data['borderOnly'] ) {
    data.ctx.fill();
  } else {
    data.ctx.stroke();
  }

  data.ctx.closePath();
}

/***********************************************************
 * Triangle super class
 *
 ***********************************************************/

// Create subclass and reassign constructor to self.
Triangle.prototype = new Shape();
Triangle.prototype.constructor = Triangle;
function Triangle( data ) {
  data = data || {};

  Shape.call( this, data );

  this.w = data.w;
  this.h = data.h;
}

// Draws a single shape to a single context
// draw() will call this with the normal canvas
// myDown will call this with the ghost canvas
Triangle.prototype.drawshape = function( data ) {
  Shape.prototype.drawshape.call(this, data); 

  // We can skip the drawing of elements that have moved off the screen:
  if (this.x > data.ctx.canvas.width || this.y > data.ctx.canvas.height) return;
  if (this.x + this.w < 0 || this.y + this.h < 0) return;

  this.drawTriangle( data );
}

Triangle.prototype.highlightBorder = function( data ) {
  Shape.prototype.highlightBorder.call(this, data);
  data['borderOnly'] = true;
  this.drawTriangle( data );
}

Triangle.prototype.drawTriangle = function( data ) {
  data.ctx.beginPath();
  // Start from the top-left point.
  data.ctx.moveTo( this.x, this.y - (this.h / 2) ); // give the (x,y) coordinates
  data.ctx.lineTo( this.x + (this.w / 2), this.y + (this.h / 2) );
  data.ctx.lineTo( this.x - (this.w / 2), this.y + (this.h / 2) );
  data.ctx.lineTo( this.x, this.y - (this.h / 2) );

  // Done! Now fill the shape, and draw the stroke.
  // Note: your shape will not be visible until you call any of the two methods.
  if ( !data['borderOnly'] ) {
    data.ctx.fill();
  } else {
    data.ctx.stroke();
  }
  data.ctx.closePath();
}

// start it up!
window.onload = function(){
  var OO_canvas = new Canvas( { canvasID: 'canvas_clickAndDragOO' });

  var r1 = new Rectangle({
    x: 800,
    y: 700,
    w: 40,
    h: 70,
    fill: '#FFC02B'
  });
  OO_canvas.addContentObject( r1 );

  var r2 = new Rectangle({
    x: 780,
    y: 780,
    w: 70,
    h: 40,
    fill: '#2BB8FF'
  });
  OO_canvas.addContentObject( r2 );

  var t1 = new Triangle({
    x: 700,
    y: 600,
    w: 70,
    h: 40,
    fill: '#900'
  });
  OO_canvas.addContentObject( t1 );

  var t2 = new Triangle({
    x: 900,
    y: 600,
    w: 70,
    h: 40,
    fill: '#009'
  });
  OO_canvas.addContentObject( t2 );

  var c1 = new Circle({
    x: 675,
    y: 730,
    r: 30,
    fill: '#459'
  });
  OO_canvas.addContentObject( c1 );

  var c2 = new Circle({
    x: 950,
    y: 730,
    r: 30,
    fill: '#954'
  });
  OO_canvas.addContentObject( c2 );
};

Browser cache refreshing for CSS and Javascript

After reading an article on it, and having an issue at work concerning it, I put together a script that should keep CSS and JS files refreshed at a configured interval.  I’d like to get comments on it, or just make it available if needed:

Here’s the script, I stored it in a separate JS file and called it into my webpage ( a jsp page) with a script element:

/**
* killCache.js
*
* Small js script that allows for appending a "code" to URLs of CSS or JS files (or
* anything that uses a LINK element) in order to force the browser to update from the
* files and not from cache.
* /

/**
* Round a value to a given interval.
*
* For example:
* (1) if the value is 4 and the interval is 10, then the result will be 0.
* (2) if the value is 755.3 and the interval is 100, the result will be 800.
*
* @param value the current value to check and round
* @param interval the interval to round the value to
* @return the rounded value
*/
function roundTo(value, interval) {
// round the value to the nearest interval
value /= interval;
value = Math.round(value);
return value * interval;
}

/**
* Create a string 'code' that changes every INTERVAL.  this code can be appended to CSS or JS files
* that change in order to make sure the user is always getting the correct version and not a cached version.
*
* @return a string code that changes every INTERVAL so that some files are not cached.
*/
function getRefreshCode() {
var currentTime = new Date();
var intervalToUse = 30; // round minutes to nearest 30 minute interval
var updateInterval = roundTo(currentTime.getMinutes(), intervalToUse);

return updateInterval + "_" + currentTime.getHours() + "_" + currentTime.getDay();
}

/**
* Create a link element in the header where the href also contains a dynamic
* string that forces a periodic update of cache
*/
function addUpdatingLinkToHead(rel, type, href, title) {
var head = document.getElementsByTagName('head')[0];
var link = document.createElement('link');

if (rel != null) { link.rel = rel; }
link.type = type;
link.href = href + "?" + getRefreshCode(); // refresh code is added here.
if (title != null) { link.title = title; }

head.appendChild(link);
}

/**
* Create a script element in the header where the href also contains a dynamic
* string that forces a periodic update of cache
*/
function addUpdatingScriptToHead(type, src) {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');

script.type = type;
script.src = src + "?" + getRefreshCode(); // refresh code is added here.

head.appendChild(script);
}

and then I the script itself, and my CSS file to the web page like this:

<!-- Small js file that creates CSS links & js scripts and forces them to update occasionally -->
<script src="/js/killCache.js" type="text/javascript"></script>
<script type="text/javascript">
  addUpdatingLinkToHead("stylesheet", "text/css", "/theme/cssFile1.css", "Style");
  addUpdatingLinkToHead("stylesheet", "text/css", "/theme/cssFile2.css", "Style");
</script>

Firebug Lite!

I found a new tool yesterday that I hope solves a lot of my issues!  Firebug Lite! If you are a developer, you are probably familiar with firebug for firefox already.  It is a great tool for debugging just about all aspects of a web page.

I work a lot with JSF pages created using Java Pure Faces, so I do a lot of my CSS tweaking after the fact.  We create web applications that will be used by just about anyone, and unfortunately, people still use IE6 (or IE at all).  Firebug Lite allows you to add a debugging tool very similar to the Firefox Firebug tool into any browser by simply adding a <script> tag like:

<script type='text/javascript' src='http://getfirebug.com/releases/lite/1.2/firebug-lite-compressed.js'></script>

This is great!  I just had an issue last week where I needed this, but I only discovered yesterday in a tweet on twitter (follow my tweets – @mpickell).  The tweet directed me to tutorial9 where there is a nice write-up.

I’m really looking forward to using this tool, but I have found some issues with already.  I haven’t seen these documented, and I am not sure if they are problems with my configuration, so I am interested if anyone else sees the same thing.

  • I wasn’t able to get firebug lite working in IE6, and the CSS window on the right hand side didn’t work for IE7.  These are probably my fault, because I tested them through IETester, and not real installs.
  • I also tested in a full, normal install of Opera and had some issues.
  • I still need to test Chrome and Safari, but i’ll post that when i do.

So this is a great tool to have available, and I’m sure upcoming revisions will fix the issues I saw.  It is going to make life a lot easier.


Testing web pages in multiple browsers

Like most other people, I need to test my web designs on multiple browsers because of the inconsistencies between all of them regarding javascript and CSS.  Mainly CSS.  The worst offender, of course, is Internet Explorer, and I have been having a lot of trouble figuring out how to test the different versions.

MultipleIE

The first software I tried was MultipleIE.  This software worked great when i was set up with MultipleIE running IE 5, IE 5.5, and IE 6, and the normal Microsoft installation of IE 7.  After installing IE 8 b2, which required installing MultipleIE’s version of standalone IE 7, everything blew up.  IE 7 and below would no longer allow me to select any input text boxes.  IE 7 and IE 8 wouldn’t let me use combo boxes because of the pop-up blocker.

IE Tester

Since that stopped working and I could not find any information online, I tried another application called IE Tester.  I’m not sure how this one works, it seems to be encapsulated all within one application.  The install package is around 120 megs, so maybe all of the IE applications are in there.  I haven’t use it that long, but it seems pretty good.  It reflects all of the IE errors I expected my application to have based on what MultipleIE seemed to be telling me.

VM

I then went on and asked a very knowledgeable friend for his opinion, and his exact tweet was:

VMware ESXi server (free), and lots of VMs. The run MultipleIE/FF/Safari on those VMs with different versions of Java, Flash, etc

which would be the ideal solution since you could run dedicated versions of everything.  But the logistics seem like they would be a nightmare.

So my search goes on.  I am trying to determine how others do this.

update:

VM: I started using an application called VirtualBox (I found it here), owned by Sun now.  It seems to be a nice windows-based application that will allow me easy connect to my network and load a single browser.  I’m still testing, and i need to see how easy it is to clone the machine once I get a clean install.