You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ResponsiveConnector.java 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. /*
  2. * Copyright 2000-2014 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.client.extensions;
  17. import java.util.logging.Level;
  18. import java.util.logging.Logger;
  19. import com.google.gwt.core.client.JavaScriptObject;
  20. import com.google.gwt.dom.client.Element;
  21. import com.vaadin.client.BrowserInfo;
  22. import com.vaadin.client.LayoutManager;
  23. import com.vaadin.client.ServerConnector;
  24. import com.vaadin.client.ui.AbstractComponentConnector;
  25. import com.vaadin.client.ui.layout.ElementResizeEvent;
  26. import com.vaadin.client.ui.layout.ElementResizeListener;
  27. import com.vaadin.server.Responsive;
  28. import com.vaadin.shared.ui.Connect;
  29. /**
  30. * The client side connector for the Responsive extension.
  31. *
  32. * @author Vaadin Ltd
  33. * @since 7.2
  34. */
  35. @SuppressWarnings("GwtInconsistentSerializableClass")
  36. @Connect(Responsive.class)
  37. public class ResponsiveConnector extends AbstractExtensionConnector implements
  38. ElementResizeListener {
  39. /**
  40. * The target component which we will monitor for width changes
  41. */
  42. protected AbstractComponentConnector target;
  43. /**
  44. * All the width breakpoints found for this particular instance
  45. */
  46. protected JavaScriptObject widthBreakpoints;
  47. /**
  48. * All the height breakpoints found for this particular instance
  49. */
  50. protected JavaScriptObject heightBreakpoints;
  51. /**
  52. * All width-range breakpoints found from the style sheets on the page.
  53. * Common for all instances.
  54. */
  55. protected static JavaScriptObject widthRangeCache;
  56. /**
  57. * All height-range breakpoints found from the style sheets on the page.
  58. * Common for all instances.
  59. */
  60. protected static JavaScriptObject heightRangeCache;
  61. private static Logger getLogger() {
  62. return Logger.getLogger(ResponsiveConnector.class.getName());
  63. }
  64. private static void error(String message) {
  65. getLogger().log(Level.SEVERE, message);
  66. }
  67. @Override
  68. protected void extend(ServerConnector target) {
  69. // Initialize cache if not already done
  70. if (widthRangeCache == null) {
  71. searchForBreakPoints();
  72. }
  73. this.target = (AbstractComponentConnector) target;
  74. // Get any breakpoints from the styles defined for this widget
  75. getBreakPointsFor(constructSelectorsForTarget());
  76. // Start listening for size changes
  77. LayoutManager.get(getConnection()).addElementResizeListener(
  78. this.target.getWidget().getElement(), this);
  79. }
  80. /**
  81. * Construct the list of selectors that should be matched against in the
  82. * range selectors
  83. *
  84. * @return The selectors in a comma delimited string.
  85. */
  86. protected String constructSelectorsForTarget() {
  87. String primaryStyle = this.target.getState().primaryStyleName;
  88. StringBuilder selectors = new StringBuilder();
  89. selectors.append(".").append(primaryStyle);
  90. if (this.target.getState().styles != null
  91. && this.target.getState().styles.size() > 0) {
  92. for (String style : this.target.getState().styles) {
  93. selectors.append(",.").append(style);
  94. selectors.append(",.").append(primaryStyle).append(".")
  95. .append(style);
  96. selectors.append(",.").append(style).append(".")
  97. .append(primaryStyle);
  98. selectors.append(",.").append(primaryStyle).append("-")
  99. .append(style);
  100. }
  101. }
  102. // Allow the ID to be used as the selector as well for ranges
  103. if (this.target.getState().id != null) {
  104. selectors.append(",#").append(this.target.getState().id);
  105. }
  106. return selectors.toString();
  107. }
  108. @Override
  109. public void onUnregister() {
  110. super.onUnregister();
  111. LayoutManager.get(getConnection()).removeElementResizeListener(
  112. this.target.getWidget().getElement(), this);
  113. }
  114. /**
  115. * Build a cache of all 'width-range' and 'height-range' attribute selectors
  116. * found in the stylesheets.
  117. */
  118. private static native void searchForBreakPoints()
  119. /*-{
  120. // Initialize variables
  121. @com.vaadin.client.extensions.ResponsiveConnector::widthRangeCache = [];
  122. @com.vaadin.client.extensions.ResponsiveConnector::heightRangeCache = [];
  123. var widthRanges = @com.vaadin.client.extensions.ResponsiveConnector::widthRangeCache;
  124. var heightRanges = @com.vaadin.client.extensions.ResponsiveConnector::heightRangeCache;
  125. // Can't do squat if we can't parse stylesheets
  126. if(!$doc.styleSheets)
  127. return;
  128. var sheets = $doc.styleSheets;
  129. // Loop all stylesheets on the page and process them individually
  130. for(var i = 0, len = sheets.length; i < len; i++) {
  131. var sheet = sheets[i];
  132. @com.vaadin.client.extensions.ResponsiveConnector::searchStylesheetForBreakPoints(Lcom/google/gwt/core/client/JavaScriptObject;)(sheet);
  133. }
  134. }-*/;
  135. /**
  136. * Process an individual stylesheet object. Any @import statements are
  137. * handled recursively. Regular rule declarations are searched for
  138. * 'width-range' and 'height-range' attribute selectors.
  139. *
  140. * @param sheet
  141. */
  142. private static native void searchStylesheetForBreakPoints(
  143. final JavaScriptObject sheet)
  144. /*-{
  145. // Inline variables for easier reading
  146. var widthRanges = @com.vaadin.client.extensions.ResponsiveConnector::widthRangeCache;
  147. var heightRanges = @com.vaadin.client.extensions.ResponsiveConnector::heightRangeCache;
  148. // Get all the rulesets from the stylesheet
  149. var theRules = new Array();
  150. var IE = @com.vaadin.client.BrowserInfo::get()().@com.vaadin.client.BrowserInfo::isIE()();
  151. var IE8 = @com.vaadin.client.BrowserInfo::get()().@com.vaadin.client.BrowserInfo::isIE8()();
  152. if (sheet.cssRules) {
  153. theRules = sheet.cssRules
  154. } else if (sheet.rules) {
  155. theRules = sheet.rules
  156. }
  157. // Special import handling for IE8
  158. if (IE8) {
  159. try {
  160. for(var i = 0, len = sheet.imports.length; i < len; i++) {
  161. @com.vaadin.client.extensions.ResponsiveConnector::searchStylesheetForBreakPoints(Lcom/google/gwt/core/client/JavaScriptObject;)(sheet.imports[i]);
  162. }
  163. } catch(e) {
  164. // This is added due to IE8 failing to handle imports of some sheets for unknown reason (throws a permission denied exception)
  165. @com.vaadin.client.extensions.ResponsiveConnector::error(Ljava/lang/String;)("Failed to handle imports of CSS style sheet: " + sheet.href);
  166. }
  167. }
  168. // Loop through the rulesets
  169. for(var i = 0, len = theRules.length; i < len; i++) {
  170. var rule = theRules[i];
  171. if(rule.type == 3) {
  172. // @import rule, traverse recursively
  173. @com.vaadin.client.extensions.ResponsiveConnector::searchStylesheetForBreakPoints(Lcom/google/gwt/core/client/JavaScriptObject;)(rule.styleSheet);
  174. } else if(rule.type == 1 || !rule.type) {
  175. // Regular selector rule
  176. // IE parses CSS like .class[attr="val"] into [attr="val"].class so we need to check for both
  177. // Pattern for matching [width-range] selectors
  178. var widths = IE? /\[width-range~?=["|'](.*)-(.*)["|']\]([\.|#]\S+)/i : /([\.|#]\S+)\[width-range~?=["|'](.*)-(.*)["|']\]/i;
  179. // Patter for matching [height-range] selectors
  180. var heights = IE? /\[height-range~?=["|'](.*)-(.*)["|']\]([\.|#]\S+)/i : /([\.|#]\S+)\[height-range~?=["|'](.*)-(.*)["|']\]/i;
  181. // Array of all of the separate selectors in this ruleset
  182. var haystack = rule.selectorText.split(",");
  183. // Loop all the selectors in this ruleset
  184. for(var k = 0, len2 = haystack.length; k < len2; k++) {
  185. var result;
  186. // Check for width-range matches
  187. if(result = haystack[k].match(widths)) {
  188. var selector = IE? result[3] : result[1]
  189. var min = IE? result[1] : result[2];
  190. var max = IE? result[2] : result[3];
  191. // Avoid adding duplicates
  192. var duplicate = false;
  193. for(var l = 0, len3 = widthRanges.length; l < len3; l++) {
  194. var bp = widthRanges[l];
  195. if(selector == bp[0] && min == bp[1] && max == bp[2]) {
  196. duplicate = true;
  197. break;
  198. }
  199. }
  200. if(!duplicate) {
  201. widthRanges.push([selector, min, max]);
  202. }
  203. }
  204. // Check for height-range matches
  205. if(result = haystack[k].match(heights)) {
  206. var selector = IE? result[3] : result[1]
  207. var min = IE? result[1] : result[2];
  208. var max = IE? result[2] : result[3];
  209. // Avoid adding duplicates
  210. var duplicate = false;
  211. for(var l = 0, len3 = heightRanges.length; l < len3; l++) {
  212. var bp = heightRanges[l];
  213. if(selector == bp[0] && min == bp[1] && max == bp[2]) {
  214. duplicate = true;
  215. break;
  216. }
  217. }
  218. if(!duplicate) {
  219. heightRanges.push([selector, min, max]);
  220. }
  221. }
  222. }
  223. }
  224. }
  225. }-*/;
  226. /**
  227. * Get all matching ranges from the cache for this particular instance.
  228. *
  229. * @param selectors
  230. */
  231. private native void getBreakPointsFor(final String selectors)
  232. /*-{
  233. var selectors = selectors.split(",");
  234. var widthBreakpoints = this.@com.vaadin.client.extensions.ResponsiveConnector::widthBreakpoints = [];
  235. var heightBreakpoints = this.@com.vaadin.client.extensions.ResponsiveConnector::heightBreakpoints = [];
  236. var widthRanges = @com.vaadin.client.extensions.ResponsiveConnector::widthRangeCache;
  237. var heightRanges = @com.vaadin.client.extensions.ResponsiveConnector::heightRangeCache;
  238. for(var i = 0, len = widthRanges.length; i < len; i++) {
  239. var bp = widthRanges[i];
  240. for(var j = 0, len2 = selectors.length; j < len2; j++) {
  241. if(bp[0] == selectors[j])
  242. widthBreakpoints.push(bp);
  243. }
  244. }
  245. for(var i = 0, len = heightRanges.length; i < len; i++) {
  246. var bp = heightRanges[i];
  247. for(var j = 0, len2 = selectors.length; j < len2; j++) {
  248. if(bp[0] == selectors[j])
  249. heightBreakpoints.push(bp);
  250. }
  251. }
  252. // Only for debugging
  253. // console.log("Breakpoints for", selectors.join(","), widthBreakpoints, heightBreakpoints);
  254. }-*/;
  255. private String currentWidthRanges;
  256. private String currentHeightRanges;
  257. @Override
  258. public void onElementResize(ElementResizeEvent event) {
  259. int width = event.getLayoutManager().getOuterWidth(event.getElement());
  260. int height = event.getLayoutManager()
  261. .getOuterHeight(event.getElement());
  262. com.google.gwt.user.client.Element element = this.target.getWidget()
  263. .getElement();
  264. boolean forceRedraw = false;
  265. // Loop through breakpoints and see which one applies to this width
  266. currentWidthRanges = resolveBreakpoint("width", width,
  267. event.getElement());
  268. if (!"".equals(currentWidthRanges)) {
  269. this.target.getWidget().getElement()
  270. .setAttribute("width-range", currentWidthRanges);
  271. forceRedraw = true;
  272. } else {
  273. element.removeAttribute("width-range");
  274. }
  275. // Loop through breakpoints and see which one applies to this height
  276. currentHeightRanges = resolveBreakpoint("height", height,
  277. event.getElement());
  278. if (!"".equals(currentHeightRanges)) {
  279. this.target.getWidget().getElement()
  280. .setAttribute("height-range", currentHeightRanges);
  281. forceRedraw = true;
  282. } else {
  283. element.removeAttribute("height-range");
  284. }
  285. if (forceRedraw) {
  286. forceRedrawIfIE8(element);
  287. }
  288. }
  289. /**
  290. * Forces IE8 to reinterpret CSS rules.
  291. * {@link com.vaadin.client.Util#forceIE8Redraw(com.google.gwt.dom.client.Element)}
  292. * doesn't work in this case.
  293. *
  294. * @param element
  295. * the element to redraw
  296. */
  297. private void forceRedrawIfIE8(Element element) {
  298. if (BrowserInfo.get().isIE8()) {
  299. element.addClassName("foo");
  300. element.removeClassName("foo");
  301. }
  302. }
  303. private native String resolveBreakpoint(String which, int size,
  304. Element element)
  305. /*-{
  306. // Default to "width" breakpoints
  307. var breakpoints = this.@com.vaadin.client.extensions.ResponsiveConnector::widthBreakpoints;
  308. // Use height breakpoints if we're measuring the height
  309. if(which == "height")
  310. breakpoints = this.@com.vaadin.client.extensions.ResponsiveConnector::heightBreakpoints;
  311. // Output string that goes into either the "width-range" or "height-range" attribute in the element
  312. var ranges = "";
  313. // Loop the breakpoints
  314. for(var i = 0, len = breakpoints.length; i < len; i++) {
  315. var bp = breakpoints[i];
  316. var min = parseInt(bp[1]);
  317. var max = parseInt(bp[2]);
  318. if(min && max) {
  319. if(min <= size && size <= max) {
  320. ranges += " " + bp[1] + "-" + bp[2];
  321. }
  322. } else if (min) {
  323. if(min <= size) {
  324. ranges += " " + bp[1] + "-";
  325. }
  326. } else if (max) {
  327. if (size <= max) {
  328. ranges += " -" + bp[2];
  329. }
  330. }
  331. }
  332. // Trim the output and return it
  333. return ranges.replace(/^\s+/, "");
  334. }-*/;
  335. }