Category Archives: design

Widgets in GWT Cell Tables

I have recently had the need to embed widgets into a GWT cell table.  I have read about the flyweight pattern, and specifically why cell tables are designed to not have widgets in them, but… when you have a requirement to complete you just need to do it.

I went through two iterations of this design and I am pretty happy with the final design.  I took some ideas from GWT Cell Widgets like EditTextCell as well as some experience from the first not-so-great implementation i created.  If you are new to Cells, it is important to realize that the Cell is instantiated one time, but called (with different data) for each row that is rendered.  This is why it is necessary to maintain a map of this cells data.

Here are some design ideas for this “WidgetCell”

  • Widget creation is expensive, so a widget for each row is only created one time and then stored in a cache for reuse.  This helps with the speed of the table rendering.
  • Widgets are never attached to the DOM.  They exist to be manipulated in memory and then have their HTML extracted and pushed into the DOM.
  • Events from the Widgets, therefore, are not handled.  If you need them you will need to change this implementation a little and pass them through the cell (onBrowserEvent method).  Cell events ARE handled (or can be in subclasses), which was enough for my purposes.
  • Finally, the widgets that I am using need to load data from the backend, so this “WidgetCell” populates the cells asynchronously.  This helps speed up initial table rendering as well.

One important thing to note on that last bullet: Browsers have a limit on the number of HTTP requests that can be active at a given time.  Additional requests (from my experience, i didn’t look into it deeply) are getting queued and block the UI from any other HTTP requests until they are cleared from the queue.  This means anything, like navigation, that requires an HTTP request will be queued and block the user interface.  The best solution I have found for this is to pass all related HTTP requests, like the ones for this widget, through a common RPC handler that performs queuing.  This can limit that data providers allowable HTTP requests, leaves additional HTTP connections available, and allows for cancelling uncalled or in-process requests if navigation changes prior to the table completely populating.

This WidgetCell is an abstract class.  It is meant to be extended, with the implementation defining the widget and handling the widget’s data.  The data can be stored in a map with an entry per row.  The abstract methods all pass through either the CellContext, the Cell context’s key, or the Cell index, depending on that is needed for that method.

Important things to remember:

  1.  As I mentioned earlier, this is a method for displaying widgets into Cells.  The widget lifecycle is NOT respected.  There is no attaching and detaching, and events are not handled from the widget.
  2. Widgets that change themselves over their lifetime will NOT have those changes reflected in the cell unless you take special care to manually reflect those changes in the DOM.  An example is a progress bar.  It will be rendered to the DOM, but any updates need to be re-rendered to the DOM manually because the widget is not attached.

Here is the code.  I am sure there will be questions that i can handle in the comments.  Additionally, I need to thank Colin Alworth at Sencha for comments and pointing out pitfalls that helped direct this final design, and David Maddison’s The GWT rendering process post for more insight on the widget lifecycle.

Here is a file with the code: Textfile with WidgetCell Code


import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.cell.client.EditTextCell;
import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.safehtml.client.SafeHtmlTemplates;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.ui.RequiresResize;
import com.google.gwt.user.client.ui.Widget;

/**
 * A Cell for adding Widgets into CellTables. A single instance of a cell is used for a column, with the render method
 * being called for each new cell in a new row. data changes per row must come in through the render method (via the
 * setValue method in super)
 * 
 * The approached used here:
 * <ol>
 * <li>Keep a cache of widgets so only a finite number are made, and they are reused to minimize expensive widget
 * creation (this is based on the method GWT's {@link EditTextCell} uses to maintain data.)</li>
 * <li>The widgets are never attached to the DOM, they are only used for manipulating them and extracting their HTML</li>
 * <li>Events from the widgets, therefore, are not handled. Only events on the cells themselves. However, if needed, the
 * cell events can be passed along to the widgets in the reuse-map and handled. (the context contains the key for the
 * reuse-map)</li>
 * <li>For ajax use, only a wrapper (placeholder) is laid down in the DOM. This placeholder is replaced when the widget
 * HTML when it is ready.</li>
 * </ol>
 * 
 * @author mpickell
 * @version 1.0
 * @param <W>
 *            the widget that is to be used in this cell
 * @param <D>
 *            the type of data used for this widget. This is passed in through the render method for each cell created.
 * 
 * @see <b>For more information on this implementation:</b>
 *      http://www.sencha.com/forum/showthread.php?196112-Widget-Rendered-Grid/page2
 * 
 *      TODO: rename to AbstractClickableWidgetCell?
 * 
 */
public abstract class ClickableWidgetCell<W extends Widget, D> extends AbstractCell<D> implements RequiresResize {

    /**
     * Widget generator for use by this cell renderer. This used as a factory that is used when a new widget is needed.
     * 
     * @param <C>
     *            widget type returned
     */
    protected interface ClickableWidgetCellWidgetFactory<C extends Widget> {
        C createWidget();
    }

    private static int uniqueKeyForWidgetID = 0;

    /**
     * Widget's displaying the table may enlarge it. If this is non-null, it will be resized when all widgets display.
     */
    private RequiresResize containerToResizeAfterAllWidgetsDisplay;

    /**
     * map of widgets used in this column. Once created, the widget is cached so it doesn't need to be created again.
     * The key here is the context.getKey() object, which uniquely IDs a row. (reuse-map is based on design of GWT's
     * {@link EditTextCell})
     */
    private Map<Object, W> reusableWidgetsMap = new HashMap<Object, W>();
    private Map<Object, String> domPlaceholderIds = new HashMap<Object, String>();

    /**
     * This cell can be used as a non-clickable cell as well. this boolean will block the click and keydown events when
     * set to true via the setter.
     */
    private boolean blockClickableEvents = false;

    /**
     * The HTML templates used to render the cell placeholder.
     */
    interface ClickableWidgetCellPlaceholderTemplate extends SafeHtmlTemplates {
        /**
         * The template for the widget placeholder for this Cell, which includes styles and a value.
         * 
         * @param id
         *            the id for this placeholder template
         * @param value
         *            the safe value. Since the value type is {@link SafeHtml}, it will not be escaped before including
         *            it in the template. Alternatively, you could make the value type String, in which case the value
         *            would be escaped.
         * @return a {@link SafeHtml} instance of a simple container. The container position is set to relative so that
         *         any value placed inside can use absolute positioning.
         */
        @SafeHtmlTemplates.Template("<div style='position:relative;' id='{0}'>{1}</div>")
        SafeHtml cell(String id, SafeHtml value);
    }

    /**
     * Create a singleton instance of the templates used to render the cell.
     */
    private static ClickableWidgetCellPlaceholderTemplate placeholderTemplate = GWT
            .create(ClickableWidgetCellPlaceholderTemplate.class);

    /**
     * Construct a new ClickableWidgetCell
     */
    public ClickableWidgetCell() {
        super("click", "keydown");
    }

    /**
     * @return a factory that creates the default widget type, with any necessary setup, that is used by this Dto.
     */
    protected abstract ClickableWidgetCellWidgetFactory<W> getWidgetFactory();

    /**
     * Trigger any asynchronous calls that are needed to get data, if needed, and then update the widget. When update is
     * complete, execute command to push widget HTML to DOM.
     * 
     * @param widget
     *            the widget that needs to be updated
     * @param onDataReadyCallback
     *            the command to trigger when the widget is updated. This command displays the widget in the page
     */
    protected abstract void updateWidget(final W widget, final Context cellContext, final D data,
            final Command onDataReadyCallback);

    /**
     * After the widget is displayed, handle any clean up (like resize to fill cell maybe). This is a default impl.
     * 
     * @param widget
     *            the widget that was displayed in the DOM
     * @param cellContext
     *            this widget's context
     * @param data
     *            the data used when displaying this widget.
     */
    protected void finalizeWidgetAfterDisplayInGrid(final W widget, final Context cellContext, final D data) {
    }

    /**
     * When resize is triggered on this cell (passed down from the grid) the cell implementation can handle it. This is
     * the default impl.
     * 
     * @param widget
     *            a widget from the reuse queue
     * @param cellContextKey
     *            this widget's context
     * @param placeholderId
     *            the placeholder ID for this widget in the DOM, so the HTML can be updated.
     */
    protected void handleWidgetOnWindowResize(W widget, Object cellContextKey, String placeholderId) {
    }

    /**
     * Default implementation.
     * 
     * This method returns HTML to be placed in the page while waiting for the widget to be displayed. If the widget
     * requires service calls, this will be displayed until the service calls return and the onDataReadyCallback command
     * is executed.
     * 
     * @return a SafeHtml object containing whatever HTML should be displayed while the main widget is being updated.
     *         This will be completely removed and placed by the widget HTML when the updateWidget method calls the
     *         onDataReadyCallback.
     */
    protected SafeHtml getWidgetUpdatingHtml() {
        return new SafeHtml() {
            private static final long serialVersionUID = 1L;

            @Override
            public String asString() {
                return "<i>Loading...</i>";
            }
        };
    }

    @Override
    public void onBrowserEvent(Context context, Element parent, D widgetDataDto, NativeEvent event,
            ValueUpdater<D> valueUpdater) {
        super.onBrowserEvent(context, parent, widgetDataDto, event, valueUpdater); // Handles user press actual
                                                                                   // ENTER key
        if ("click".equals(event.getType())) {
            onEnterKeyDown(context, parent, widgetDataDto, event, valueUpdater);
        }
    }

    @Override
    protected void onEnterKeyDown(Context context, Element parent, D widgetDataDto, NativeEvent event,
            ValueUpdater<D> valueUpdater) {
        if (!blockClickableEvents && valueUpdater != null) {
            valueUpdater.update(widgetDataDto);
        }
    }

    @Override
    public void render(final Context context, final D widgetDataDto, final SafeHtmlBuilder sb) {
        /*
         * Widgets are reused so that their (expensive) creation is not performed each time. After creation, they are
         * attached and detached as needed and re-added to the page.
         */
        Object rowKey = context.getKey();

        /*
         * Either get the existing widget from the cache or create it.
         */
        W wfc = reusableWidgetsMap.get(rowKey);
        if (wfc == null) {
            // Create once and store.
            wfc = this.getWidgetFactory().createWidget();
            reusableWidgetsMap.put(rowKey, wfc);
        }
        final W wfcAsFinal = wfc;

        /*
         * Setup a unique key for the placeholder in this row
         */
        if (!domPlaceholderIds.containsKey(context.getKey())) {
            setupPlaceholderId(context);
        }

        /*
         * Allow the Dto to update the widget, possibly after asynchronous calls to service. Once updated, the command
         * is (i.e., has to be) executed to finalize the displaying of the cell.
         * 
         * This update is triggered as a GWT "thread" using the deferred command so that the render method can complete
         * quickly. The update and display of the widget can be deferred safely because everything needed for it is
         * packaged in the updateWidget method
         */
        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
            @Override
            public void execute() {
                updateWidget(wfcAsFinal, context, widgetDataDto, new Command() {
                    @Override
                    public void execute() {
                        ClickableWidgetCell.this.displayWidget(context, widgetDataDto);
                    }
                });
            }
        });

        /*
         * Put the placeholder in the table so we have a location to go to later. Put the "busy" html in there.
         */
        sb.append(placeholderTemplate.cell(domPlaceholderIds.get(context.getKey()), getWidgetUpdatingHtml()));
    }

    private void displayWidget(final Context cellContext, final D widgetDataDto) {
        Element e = getElementsBySelectorJSNI("#" + domPlaceholderIds.get(cellContext.getKey()));
        if (e != null && reusableWidgetsMap.containsKey(cellContext.getKey())
                && reusableWidgetsMap.get(cellContext.getKey()).getElement() != null) {
            e.setInnerHTML(reusableWidgetsMap.get(cellContext.getKey()).getElement().getString());

            /*
             * The widget is now displayed. Allow the implementation to perform any adjustments to it. The
             * implementation can use the domPlaceholderIds map (via getter) to replace the HTML in the page since the
             * element in the widget is never attached to the page.
             * 
             * Changes to the widget will be reflected in the widget map, placeholder ID can be used (via getter, as a
             * string so it cannot be changed) to push any updated HTML to the page.
             */
            Scheduler.get().scheduleDeferred(new ScheduledCommand() {
                @Override
                public void execute() {
                    ClickableWidgetCell.this.finalizeWidgetAfterDisplayInGrid(reusableWidgetsMap.get(cellContext.getKey()),
                            cellContext, widgetDataDto);
                }
            });

            /*
             * This is a widget that might be expanding the table. Call on the resize container to resize.
             */
            if (containerToResizeAfterAllWidgetsDisplay != null) {
                containerToResizeAfterAllWidgetsDisplay.onResize();
            }
        }

    }

    /**
     * Query for elements using selectors
     * 
     * @param selectors
     *            a string containing one or more CSS selectors separated by commas
     * @return a JS array of elements.
     * 
     * @see https://developer.mozilla.org/en-US/docs/DOM/Document.querySelector
     * @see http://www.w3.org/TR/selectors-api/
     */
    protected native Element getElementsBySelectorJSNI(String selector) /*-{
		return $doc.querySelector(selector);
    }-*/;

    /**
     * Create a unique ID for a placeholder that will be placed in the cell on render.
     * 
     * @param context
     */
    private void setupPlaceholderId(Context context) {
        if (uniqueKeyForWidgetID >= Integer.MAX_VALUE) {
            // housekeeping.
            uniqueKeyForWidgetID = 0;
        } else {
            uniqueKeyForWidgetID++;
        }
        domPlaceholderIds.put(context.getKey(), "WidgetCellID_" + uniqueKeyForWidgetID);
    }

    /**
     * If true, this will block the clickable events and this cell will simply render the widget without accepting
     * events.
     * 
     * @param blockClickableEvents
     */
    public void setBlockClickableEvents(boolean blockClickableEvents) {
        this.blockClickableEvents = blockClickableEvents;
    }

    @Override
    public void onResize() {
        /*
         * on resize of the page, update the widget to the new cell size if necessary.
         */
        if (reusableWidgetsMap != null && !reusableWidgetsMap.isEmpty()) {
            for (Entry<Object, W> entry : reusableWidgetsMap.entrySet()) {
                ClickableWidgetCell.this.handleWidgetOnWindowResize(entry.getValue(), entry.getKey(),
                        domPlaceholderIds.get(entry.getKey()));
            }
        }
    }

    /**
     * Get the ID for the widget placeholder in the page. the innerHtml of this element is the widget HTML.
     * 
     * @param cellContextKey
     * @return
     */
    protected String getDomPlaceholderId(Object cellContextKey) {
        return domPlaceholderIds.get(cellContextKey);
    }

    /**
     * Set the container of this grid so it can be resized when all widgets display.
     * 
     * @param containerToResizeAfterAllWidgetsDisplay
     *            the container that implements RequiresResize & IsWidget
     */
    public void setContainerToResizeAfterAllWidgetsDisplay(RequiresResize containerToResizeAfterAllWidgetsDisplay) {
        this.containerToResizeAfterAllWidgetsDisplay = containerToResizeAfterAllWidgetsDisplay;
    }

}

Share

GWT & mvp4g: Displaying history on the client side.

I’m currently working on a task to display and persist history using GWT and mvp4g.  There are many ways of doing this, but here is how i’m going it.  Maybe this will help you with an additional approach, or maybe you can help me do it better.

Implementation Attempts

1st Pass: GWT’s History Event

First I attempted to use GWT’s History ValueChange event to capture changes happening in history.  I run into trouble here right away.  This event only seemed to capture history changes that occurred when I called History.newItem(...) myself; mvp4g was not triggering it.  I search around on the internet and didn’t find any explanation of what this event was supposed to be doing, or how mvp4g handled it, but apparently mvp4g was not triggering this event for its history changes.  This method was out.

UPDATE: For some reason it eluded me in my searches until after my final design, but you could create your own PlaceService, which is using the ValueChange event in order to re-create views from history.  There are a lot of options here, which i am going to take a look at because it will most likely change the design I am currently using.  See a write-up here.

2nd Pass: hashchange Event

I am lucky to be supporting only most recent browsers, so the next method I attempted was to use the hashchange event to capture every change made in the browser.  This worked very well and caught everything.

In the end, i did not use it because it is detached from the mvp4g History Converters.  It is cleaner to keep all of the history implementation in one place so each converter can define its own behavior, and that behavior is maintained along with those converters.  I would have had to catch a change and then query into one of the converters to get additional information on resolving descriptions, etc.   That is messy enough, but then there is the issue of the converters not being programmatically accessible (I couldn’t figure out how to get them from mvp4g… i would love to know if someone can tell me), and they do not have an eventBus instance.

Anyway, I did not end up using this approach in my design but here is that code if it can help you out.  At the very least, it was good to get to know this hashchange event.  GWT does not have any built in hooks to this event, so I created a new EventHandler to encapsulate it and make it injectable into wherever it is needed.

Here’s the class I created:

import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.event.shared.EventHandler;
import com.google.gwt.event.shared.GwtEvent;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.event.shared.HasHandlers;
import com.google.inject.Singleton;
import com.mvp4g.client.event.BaseEventHandler;

/**
 * EventHandler for hash change events. This is an eventhandler so that it is a singleton that can be injected where
 * needed, and can access the eventbus if events are sent that way.
 */
@Singleton
@com.mvp4g.client.annotation.EventHandler
public class HashChangeEventEmitter extends BaseEventHandler implements HasHandlers {

 // handlers
 private HandlerManager handlerManager;

 protected HashChangeEventEmitter() {
 handlerManager = new HandlerManager(this);
 }

  @Override
 public void setEventBus(EventBus eventBus) {
 super.setEventBus(eventBus);

     Scheduler.get().scheduleDeferred(new ScheduledCommand() {
         @Override
         public void execute() {
             registerHashChangedEventListenerInDOM();
         }
     });
 }

 private void fireHashChangedEventFromDOM(String oldUrl, String newUrl) {
     fireHashChangedEvent(new HashChangedEvent(oldUrl, newUrl));
 }

 /**********************************************************
 * Event registration and emitting
 **********************************************************/

 private void fireHashChangedEvent(HashChangedEvent event) {
     if (handlerManager.getHandlerCount(HashChangedEvent.TYPE) > 0) {
         handlerManager.fireEvent(event);
     }
 }

 /** {@inheritDoc} */
 public HandlerRegistration addHashChangedEventHandler(HashChangedEventHandler handler) {
     return handlerManager.addHandler(HashChangedEvent.TYPE, handler);
 }

 @Override
 public void fireEvent(GwtEvent event) {
     handlerManager.fireEvent(event);
 }

 /**********************************************************
 * Event definition
 **********************************************************/
 public static interface HashChangedEventHandler extends EventHandler {
     void onHashChanged(HashChangedEvent event);
 }

 public static class HashChangedEvent extends GwtEvent {

 public static Type TYPE = new Type();

 private final String oldUrlHash;
 private final String oldUrlRaw;
 private final String newUrlHash;
 private final String newUrlRaw;

 public HashChangedEvent(String oldUrlRaw, String newUrlRaw) {
     super();
     this.oldUrlHash = extractHash(oldUrlRaw);
     this.newUrlHash = extractHash(newUrlRaw);

     this.oldUrlRaw = oldUrlRaw;
     this.newUrlRaw = newUrlRaw;
 }

 private String extractHash(String url) {
     return url.contains("#") ? url.substring(url.indexOf('#')) : "";
 }

 @Override
 public Type getAssociatedType() {
     return TYPE;
 }

 @Override
 protected void dispatch(HashChangedEventHandler handler) {
     handler.onHashChanged(this);
 }

 /** @return just the hash from the old URL */
 public String getOldUrlHash() {
     return oldUrlHash;
 }

 /** @return the entire old URL */
 public String getOldUrlRaw() {
     return oldUrlRaw;
 }

 /** @return just the hash from the new URL */
 public String getNewUrlHash() {
     return newUrlHash;
 }

 /** @return the entire new URL */
 public String getNewUrlRaw() {
     return newUrlRaw;
 }

 }

 private final native void registerHashChangedEventListenerInDOM()
/*-{
 var hashChangedEventEmitter = this;

   $wnd
    .addEventListener(
        "hashchange",
        function(event) {
       hashChangedEventEmitter.@<yourPathToEmitter>.HashChangeEventEmitter::fireHashChangedEventFromDOM(Ljava/lang/String;Ljava/lang/String;)(event.oldURL, event.newURL);
       });
 }-*/;

}

The event is a standard GwtEvent, and the class can be injected wherever it is needed like this:

    @Inject
    protected HashChangeEventEmitter hashChangeEventEmitter;

Final Solution: Instrumenting the History Converters

mvp4g handles history in GWT through History Converters, which uses the GWT History framework.  These allow you to attach history entries directly to EventBus calls.  Anytime you trigger a certain event, you can specify how to capture (and recreate) the resulting action in history.

The design I’m currently using creates an abstract history converter that contains a common method called at the end of every history converter implementation’s event bus call (the one creating the token to save for this history entry).  This method, in turn, calls a couple of abstract methods implemented in the history converters that resolve any information needed to display what this history entry is and the history token itself.  This information is then passed to the presenter of the history display view.  This instruments each history converter and requires all new history converters implement these methods so that the history display is kept updated.

The presenter for the history view is injected the same way the HashChangeEventEmitter was injected in the 2nd pass above.  This presenter then handles pushing the history information to the backend as well as updating the view.

Re-evaluation and design forthcoming

It eluded me in my original google searches when I was working on my original designs, but I am going to revisit this and look at implementing it in a cleaner way using a mvp4g Custom PlaceService.  This is the place that has a collection of all of the history converters and is a central point that can be used to implement exactly what I’m trying to do without needing to clutter up the history converters. It is exactly what I was look for, and knew was there somewhere, but missed…

I will update this post after that…

Share

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!

Share

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: Here is a link to a zip file demo.  Unzip it and open index.html in your browser.  I’ll add it to run here at some point..

UPDATE 4:  Not sure why I did not do this earlier… but here is is now running

/*
 * 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 );
};
Share

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>
Share

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.

Share

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.

Share