Friday, February 1, 2013

CSS Resources: @Import and @ImportedWithPrefix

CSS Resources

@Import and @ImportedWithPrefix

A CSS Resource can import other CSS resources. GWT supplies two annotations for this: @Import and @ImportedWithPrefix.
Unfortunately, the online examples and tutorials that I have found for this are woefully incomplete. They all suffer from what I have found to be an ever-increasing approach to programming discussions and documentation: they have either been written hypothetically (i.e., the code they list was never actually run), or, if they did come from a working example, the small details crucial to understanding and implementing the concept have been left out.
Plus, to make things even more confusing, the @Import annotation does not produce the same effect as using @import within a CSS file. When used in a CSS file associated with a CssResource, @import doesn't seem to do anything.
So, below is my attempt at describing these two annotations and how they are used.
A CssResource sub-interface can have a prefix automatically prepended to its style class names by annotating it with the @ImportedWithPrefix("prefix") annotation. As a result, all the style class names will have the prefix prepended, regardless of whether the CSS resource is imported into another, of just used without importing it anywhere(a better name for the annotation might have been @PrefixedWith). In the associated CSS file, however, you should write the class names without the prefix (which would match the method names in the interface).
Another CSS resource can import one or more additional CssResource classes by creating a method that returns a CssResource, annotated with @Import. The value supplied for the annotation can be a single class object for another CssResource, or an array of those class objects. Given that the imported class names will have the prefix regardless of whether they're imported anywhere, the real benefit of this is that the associated stylesheet can modify the rules involving the imported class name (but, they would now use the prefix).
For any class names that the importing CSS does want to use, there must be an associated method in the interface (but, without any prefix). It appears that the GWT compiler, as of GWT 2.5, will allow the CSS file to have any prefixed class names as long as the end part of the name is declared as a method in the interface, even if the specific prefix applied doesn't match anything that was imported.
@ImportedWithPrefix("prefix")
public interface CssResourceNameToBePrefixed extends CssResource {
 public String accessorMethods();
}
Within the associated CSS file, all the style class names used in the interface must be defined, but without the prefix. When the resource is imported into another, the resulting class definitions will have the prefix (of course, you won't be able to tell, since they will be obfuscated).
Note that by doing this, you are locking this resource into always using the specified prefix.
public interface BundleClass extends ClientBundle {

 @Source(value="CssResourceToBePrefixedCssFilePath")
 public CssResourceNameToBePrefixed accessorMethods();
 
 @Import(value=Class<? extends CssResource>)
 @Source(value="ImportingCssResourceCssFilePath")
 public ImportingCssResourceClassName accessorMethods();
}
The CSS file associated with the new CSS resource can also contain rules involving the imported classes, but now those rules must include the prefix.
But, to use the prefixed rules, your client bundle must have an instance of the CSS resource that originally defined them, not just the one that imported them. So, the client bundle containing the code snippet above, you must also define a method to retrieve the original CSS resource (the one with @ImportedWithPrefix). In fact, a CSS resource that uses the @ImportedWithPrefix annotation doesn't even have to be imported into any other - you can just invoke the accessor methods to retrieve the obfuscated name, which will be different from the same name defined in a different CSS resource with a different prefix or no prefix.
But, if this resource is imported into another, any rules added by the importing resource's CSS file will be applied also, since that caused the CSS to be generated and injected.

A Working Example

The Eclipse Project
Note that in order to minimize the file size, the gwt-servlet.jar file is not included. After unzipping and importing into an Eclipse workspace, you can usually reinstate it by going to the project's properties, under the Google ... Web Toolkit sections, removing the check from Use Google Web Toolkit, then OK. Then go back through the same process to put the check back and OK again.
You can try the code below to see all three CSS resources in action. There are embedded notes regarding code to comment out to try it without any instance of the Styles resource - they must be done as a group.
/* Styles.css */
@external .gwt-HTML;

.special { font-size: 14pt; }

/* Try commenting out the two x-special rules below */
.a-special { font-size: 18pt; }

.b-special { font-size: 22pt; }

/* Not related to the example, just to make things look nicer */
html .gwt-HTML {
 margin-top: 12px;
 border: 1px solid blue;
 padding: 12px;
 border-radius: 6px;
}
// Styles.java
package whatever.client;
import com.google.gwt.resources.client.CssResource;
public interface Styles extends CssResource {
 public String special();
}
/* AStyles.css */
.special { background-color: #ffaaaa; }
// AStyles.java
package whatever.client;

import com.google.gwt.resources.client.CssResource;
import com.google.gwt.resources.client.CssResource.ImportedWithPrefix;

@ImportedWithPrefix("a")
public interface AStyles
//extends Styles  // would also work with this instead of below
extends CssResource {
 public String special();
}
/* BStyles.css */
.special { background-color: #aaaaff; }
// BStyles.java
package whatever.client;

import com.google.gwt.resources.client.CssResource;
import com.google.gwt.resources.client.CssResource.ImportedWithPrefix;

@ImportedWithPrefix("b")
public interface BStyles
//extends Styles  // would also work with this instead of below
extends CssResource {
 public String special();
}
// SomeResources.java
package whatever.client;

import com.google.gwt.core.client.GWT;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource.Import;

public interface SomeResources extends ClientBundle {
 
 /* Try commenting out the three lines below, after
  * commenting out the prefixed lines in the Styles.css file
  */
 @Import({AStyles.class, BStyles.class})
 @Source("Styles.css")
 public Styles styles();
 
 @Source("AStyles.css")
 public AStyles aStyles();
     
 @Source("BStyles.css")
 public BStyles bStyles();
 
 public static final SomeResources INSTANCE =
  GWT.create(SomeResources.class);
    
}
In the code below, the lines marked with /**/ are to be commented out to try it without any instance of the Styles class.
// SomeResources.java
package whatever.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.user.client.ui.*;

public class CssResourceImports implements EntryPoint {
 
 private static final SomeResources resources
  = SomeResources.INSTANCE;
 
 private static final AStyles aStyles = resources.aStyles();
 private static final BStyles bStyles = resources.bStyles();
 private static final Styles styles = resources.styles();    /**/

 public void onModuleLoad() {
  
  aStyles.ensureInjected();
  bStyles.ensureInjected();
  styles.ensureInjected();                                  /**/
  
  FlowPanel panel = new FlowPanel();
  
  HTML special = new HTML("Special");                       /**/
  special.addStyleName(styles.special());                   /**/
  panel.add( special );                                     /**/
  
  HTML aSpecial = new HTML("A Special");
  aSpecial.addStyleName(aStyles.special());
  panel.add( aSpecial );
  
  HTML bSpecial = new HTML("B Special");
  bSpecial.addStyleName(bStyles.special());
  panel.add( bSpecial );
  
  RootPanel.get().add( panel );
 }
}

Wednesday, January 9, 2013

Scalable IFrame Video

Scalable Video

It's easy to scale <img> and <video> elements with window width. The "max-height: 100% trick" makes this task straightforward:

img.scalable, video.scalable {
  max-width: 100%;
}

Width (%):

It's not really a trick - it uses features that have been around in HTML more or less forever. If you give an image height and width attributes, it will scale to that size, even if that distorts the aspect ratio. However, if you give it only one of the two values, the other will scale to match, keeping the original aspect ratio. This behavior carries over to measurements provided via CSS rather than as tag attributes.

When you use max-width: 100%, and the container element becomes narrower than the image's or video's natural size, it becomes a similar constraint on the width, and the height scales to match. (One thing that this means is that you're not necessarily limited to using 100%. You can set max-width to whatever percentage you wish.)

But, when the video is embedded in an <iframe> element, in particular an iframe that is from a different server, things become more difficult. You can't get inside a foreign iframe to stylize it's elements, and iframes don't have a concept of a natural aspect ratio.

Yet another issue is that the space needed by a video won't actually have a constant aspect ratio if it has controls - the controls have a fixed height regardless of the width.

I have seen solutions that rely on padding-bottom using a percentage, taking advantage of the fact that vertical padding and margins expressed in percentages are percentages of the element's width, not its height. My experience has been that this usually requires a bit of tinkering to get right, and adds some not-so-intuitive markup. I would like to propose what I believe is a simpler and more robust approach. While it does add some markup, I think that it's easier to understand what it does.

The video embedded here uses a that approach. Rather than mess around trying to break my template styles in order that the column be resizable, I added a text box that you can use to enter a width (in percent of the column width) for the video iframe. If you want to see a more dynamic, window-width-based version, see the Scalable Iframe Video page at my site.

Mocking the Video

Not, I'm not going to make fun of it - I'm going to create a mockup using transparent GIF images. The total space occupied by the video will usually consist of two pieces: the video itself, which has a fixed aspect ratio, and a controller, which will have a fixed height. The combination of those elements will therefore not have a fixed aspect ratio.

But, we can build a div using two transparent images with the expected sizes. In the model I tested, the video has a 4/3 aspect ratio, so I used a 420 * 315 pixel image as a mock. I also used a 420 * 36 pixel image to mock the controls. The key aspect of the associated CSS is that the mock image for the video itself has max-width: 100% and no height specification, but the controls image has the height fixed at 36px.

I put both of these in in a container div which in turn sits in a larger, outer container div, along with the iframe for the video.

The combined height of the two mock images determines the height of their immediate container. The width of that container, however, is determined by its parent.

The parent that contains the assembled mock div and the iframe has position:relative;, in order to serve as an offset parent to its children. The mock div is unpositioned, so that it sits at 0,0 by default, and also serves to determine the height of the container. The iframe, however is absolutely positioned at 0,0, and has height and width of 100%, which is what makes it assume the aspect ratio of the container.

<div class="vidFrame">
  <div class="vid-box">
    <img src="vid-mock-3x4.gif" class="vid-mock">
    <img src="vid-controls.gif" class="vid-controls">
  </div>
  <iframe width="420" height="315"
    src="http://www.youtube.com/embed/0WyhMV81dro"
    frameborder="0"
    allowfullscreen></iframe>
</div>
.vid-box {
  width: 420px;
  overflow: hidden;
  max-width: 100%;
}
.vid-box img {
  display: block;
  width: 100%;
}
.vid-mock {
  max-width: 100%;
}
.vid-controls {
  height: 36px;
  max-width: 100%;
}
.vidFrame {
  position: relative;
  width: 420px;
  max-width: 100%;
}
.vidFrame iframe {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}