package com.vaadin.addon.responsive.client;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.dom.client.Element;
import com.vaadin.addon.responsive.Responsive;
import com.vaadin.client.LayoutManager;
import com.vaadin.client.ServerConnector;
import com.vaadin.client.extensions.AbstractExtensionConnector;
import com.vaadin.client.ui.AbstractComponentConnector;
import com.vaadin.client.ui.layout.ElementResizeEvent;
import com.vaadin.client.ui.layout.ElementResizeListener;
import com.vaadin.shared.ui.Connect;
/**
* The client side connector for the Responsive extension.
*
* TODO It might make more sense to just make this a pure JS extension, since
* the amount of native code in this class is near 100%.
*
* @author jouni@vaadin.com
*
*/
@Connect(Responsive.class)
public class ResponsiveConnector extends AbstractExtensionConnector implements
ElementResizeListener {
private static final long serialVersionUID = -7960943137494057105L;
/**
* The target component which we will monitor for width changes
*/
protected AbstractComponentConnector target;
/**
* All the width breakpoints found for this particular instance
*/
protected JavaScriptObject widthBreakpoints;
/**
* All the height breakpoints found for this particular instance
*/
protected JavaScriptObject heightBreakpoints;
/**
* All width-range breakpoints found from the style sheets on the page.
* Common for all instances.
*/
protected static JavaScriptObject widthRangeCache;
/**
* All height-range breakpoints found from the style sheets on the page.
* Common for all instances.
*/
protected static JavaScriptObject heightRangeCache;
@Override
protected void extend(ServerConnector target) {
// Initialize cache if not already done
if (widthRangeCache == null) {
searchForBreakPoints();
}
this.target = (AbstractComponentConnector) target;
// Construct the list of selectors we should match against in the
// range selectors
String primaryStyle = this.target.getState().primaryStyleName;
StringBuilder selectors = new StringBuilder();
selectors.append("." + primaryStyle);
if (this.target.getState().styles != null
&& this.target.getState().styles.size() > 0) {
for (String style : this.target.getState().styles) {
// TODO decide all the combinations we want to support
selectors.append(",." + style);
selectors.append(",." + primaryStyle + "." + style);
selectors.append(",." + style + "." + primaryStyle);
selectors.append(",." + primaryStyle + "-" + style);
}
}
// Allow the ID to be used as the selector as well for ranges
if (this.target.getState().id != null) {
selectors.append(",#" + this.target.getState().id);
}
// Get any breakpoints from the styles defined for this widget
getBreakPointsFor(selectors.toString());
// Start listening for size changes
LayoutManager.get(getConnection()).addElementResizeListener(
this.target.getWidget().getElement(), this);
}
/**
* Build a cache of all 'width-range' and 'height-range' attribute selectors
* found in the stylesheets.
*/
private static native void searchForBreakPoints()
/*-{
// Initialize variables
@com.vaadin.addon.responsive.client.ResponsiveConnector::widthRangeCache = [];
@com.vaadin.addon.responsive.client.ResponsiveConnector::heightRangeCache = [];
var widthRanges = @com.vaadin.addon.responsive.client.ResponsiveConnector::widthRangeCache;
var heightRanges = @com.vaadin.addon.responsive.client.ResponsiveConnector::heightRangeCache;
// Can't do squat if we can't parse stylesheets
if(!$doc.styleSheets)
return null;
var sheets = $doc.styleSheets;
// Loop all stylesheets on the page and process them individually
for(var i = 0, len = sheets.length; i < len; i++) {
var sheet = sheets[i];
@com.vaadin.addon.responsive.client.ResponsiveConnector::searchStylesheetForBreakPoints(Lcom/google/gwt/core/client/JavaScriptObject;)(sheet);
}
// Only for debugging
// console.log("All breakpoints", widthRanges, heightRanges);
}-*/;
/**
* Process an individual stylesheet object. Any @import statements are
* handled recursively. Regular rule declarations are searched for
* 'width-range' and 'height-range' attribute selectors.
*
* @param sheet
*/
private static native void searchStylesheetForBreakPoints(
final JavaScriptObject sheet)
/*-{
// Inline variables for easier reading
var widthRanges = @com.vaadin.addon.responsive.client.ResponsiveConnector::widthRangeCache;
var heightRanges = @com.vaadin.addon.responsive.client.ResponsiveConnector::heightRangeCache;
// Get all the rulesets from the stylesheet
var theRules = new Array();
var IE = $wnd.navigator.appName == "Microsoft Internet Explorer";
var IE8 = false;
if (sheet.cssRules) {
theRules = sheet.cssRules
} else if (sheet.rules) {
theRules = sheet.rules
IE8 = true; // Only browser (of supported ones) that has no cssRules property
}
// Special import handling for IE8
try {
if (IE8) {
for(var i = 0, len = sheet.imports.length; i < len; i++) {
@com.vaadin.addon.responsive.client.ResponsiveConnector::searchStylesheetForBreakPoints(Lcom/google/gwt/core/client/JavaScriptObject;)(sheet.imports[i]);
}
}
} catch(e) {
// This is added due to IE8 failing to handle imports of some sheets for unknown reason (throws a permission denied exception)
console.log("Failed to handle imports of CSS style sheet: " + sheet.href);
// Just continue parsing the other sheets
}
// Loop through the rulesets
for(var i = 0, len = theRules.length; i < len; i++) {
var rule = theRules[i];
if(rule.type == 3) {
// @import rule, traverse recursively
@com.vaadin.addon.responsive.client.ResponsiveConnector::searchStylesheetForBreakPoints(Lcom/google/gwt/core/client/JavaScriptObject;)(rule.styleSheet);
} else if(rule.type == 1 || !rule.type) {
// Regular selector rule
// IE parses CSS like .class[attr="val"] into [attr="val"].class so we need to check for both
// Pattern for matching [width-range] selectors
var widths = IE? /\[width-range~?=["|'](.*)-(.*)["|']\]([\.|#]\S+)/i : /([\.|#]\S+)\[width-range~?=["|'](.*)-(.*)["|']\]/i;
// Patter for matching [height-range] selectors
var heights = IE? /\[height-range~?=["|'](.*)-(.*)["|']\]([\.|#]\S+)/i : /([\.|#]\S+)\[height-range~?=["|'](.*)-(.*)["|']\]/i;
// Array of all of the separate selectors in this ruleset
var haystack = rule.selectorText.split(",");
// Loop all the selectors in this ruleset
for(var k = 0, len2 = haystack.length; k < len2; k++) {
var result;
// Check for width-range matches
if(result = haystack[k].match(widths)) {
var selector = IE? result[3] : result[1]
var min = IE? result[1] : result[2];
var max = IE? result[2] : result[3];
// Avoid adding duplicates
var duplicate = false;
for(var l = 0, len3 = widthRanges.length; l < len3; l++) {
var bp = widthRanges[l];
if(selector == bp[0] && min == bp[1] && max == bp[2]) {
duplicate = true;
break;
}
}
if(!duplicate) {
widthRanges.push([selector, min, max]);
}
}
// Check for height-range matches
if(result = haystack[k].match(heights)) {
var selector = IE? result[3] : result[1]
var min = IE? result[1] : result[2];
var max = IE? result[2] : result[3];
// Avoid adding duplicates
var duplicate = false;
for(var l = 0, len3 = heightRanges.length; l < len3; l++) {
var bp = heightRanges[l];
if(selector == bp[0] && min == bp[1] && max == bp[2]) {
duplicate = true;
break;
}
}
if(!duplicate) {
heightRanges.push([selector, min, max]);
}
}
}
}
}
}-*/;
/**
* Get all matching ranges from the cache for this particular instance.
*
* @param selectors
*/
private native void getBreakPointsFor(final String selectors)
/*-{
var selectors = selectors.split(",");
var widthBreakpoints = this.@com.vaadin.addon.responsive.client.ResponsiveConnector::widthBreakpoints = [];
var heightBreakpoints = this.@com.vaadin.addon.responsive.client.ResponsiveConnector::heightBreakpoints = [];
var widthRanges = @com.vaadin.addon.responsive.client.ResponsiveConnector::widthRangeCache;
var heightRanges = @com.vaadin.addon.responsive.client.ResponsiveConnector::heightRangeCache;
for(var i = 0, len = widthRanges.length; i < len; i++) {
var bp = widthRanges[i];
for(var j = 0, len2 = selectors.length; j < len2; j++) {
if(bp[0] == selectors[j])
widthBreakpoints.push(bp);
}
}
for(var i = 0, len = heightRanges.length; i < len; i++) {
var bp = heightRanges[i];
for(var j = 0, len2 = selectors.length; j < len2; j++) {
if(bp[0] == selectors[j])
heightBreakpoints.push(bp);
}
}
// Only for debugging
// console.log("Breakpoints for", selectors.join(","), widthBreakpoints, heightBreakpoints);
}-*/;
private String currentWidthRanges;
private String currentHeightRanges;
@Override
public void onElementResize(ElementResizeEvent e) {
int width = e.getLayoutManager().getOuterWidth(e.getElement());
int height = e.getLayoutManager().getOuterHeight(e.getElement());
// Loop through breakpoints and see which one applies to this width
currentWidthRanges = resolveBreakpoint("width", width, e.getElement());
if (currentWidthRanges != "") {
this.target.getWidget().getElement()
.setAttribute("width-range", currentWidthRanges);
} else {
this.target.getWidget().getElement().removeAttribute("width-range");
}
// Loop through breakpoints and see which one applies to this height
currentHeightRanges = resolveBreakpoint("height", height,
e.getElement());
if (currentHeightRanges != "") {
this.target.getWidget().getElement()
.setAttribute("height-range", currentHeightRanges);
} else {
this.target.getWidget().getElement()
.removeAttribute("height-range");
}
}
private native String resolveBreakpoint(String which, int size,
Element element)
/*-{
// Default to "width" breakpoints
var breakpoints = this.@com.vaadin.addon.responsive.client.ResponsiveConnector::widthBreakpoints;
// Use height breakpoints if we're measuring the height
if(which == "height")
breakpoints = this.@com.vaadin.addon.responsive.client.ResponsiveConnector::heightBreakpoints;
// Output string that goes into either the "width-range" or "height-range" attribute in the element
var ranges = "";
// Loop the breakpoints
for(var i = 0, len = breakpoints.length; i < len; i++) {
var bp = breakpoints[i];
var min, max;
// Do we need to calculate the pixel value?
if(bp[1] != "0" && bp[1].indexOf("px") == -1) {
min = @com.vaadin.addon.responsive.client.ResponsiveConnector::getPixelSize(Ljava/lang/String;Lcom/google/gwt/dom/client/Element;)(bp[1], element);
// Calculation failed somehow, ignore this breakpoint
// TODO inform the developer somehow?
if(min == -1) continue;
} else {
// No, we can use the pixel value directly
min = parseInt(bp[1]);
}
// Do we need to calculate the pixel value?
if(bp[2] && bp[2].indexOf("px") == -1) {
max = @com.vaadin.addon.responsive.client.ResponsiveConnector::getPixelSize(Ljava/lang/String;Lcom/google/gwt/dom/client/Element;)(bp[2], element);
// Calculation failed somehow, ignore this breakpoint
// TODO inform the developer somehow?
if(max == -1) continue;
} else {
// No, we can use the pixel value directly
max = parseInt(bp[2]);
}
if(max) {
if(min <= size && size <= max) {
ranges += " " + bp[1] + "-" + bp[2];
}
} else {
if(min <= size) {
ranges += " " + bp[1] + "-";
}
}
}
// Trim the output and return it
return ranges.replace(/^\s+/, "");
}-*/;
private static native int getPixelSize(String size, Element context)
/*-{
// Get the value and units from the size
var items = size.match(/^(\d+)?(\.\d+)?(.{1,3})/);
var val = (items[1] || 0) + (items[2] || "");
var unit = items[3].toLowerCase();
// Use a temporay measuring element to get the computed size of the relative units
if(unit == "em" || unit == "rem" || unit == "ex" || unit == "ch") {
var measure = $doc.createElement("div");
measure.style.width = size;
context.appendChild(measure);
var s = measure.offsetWidth;
context.removeChild(measure)
return s;
}
// Handle all other absolute units with basic math
var ret = -1;
switch(unit) {
case "in":
ret = val * 96;
break;
case "cm":
ret = val * 37.8;
break;
case "mm":
ret = val * 3.78;
break;
case "pt":
ret = (val * 96) / 72;
break;
case "pc":
ret = ((val * 96) / 72) * 12;
break;
}
return ret;
}-*/;
}