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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. /*
  2. * Copyright 2000-2016 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.vaadin.client.LayoutManager;
  21. import com.vaadin.client.ServerConnector;
  22. import com.vaadin.client.communication.StateChangeEvent;
  23. import com.vaadin.client.ui.AbstractComponentConnector;
  24. import com.vaadin.client.ui.layout.ElementResizeEvent;
  25. import com.vaadin.client.ui.layout.ElementResizeListener;
  26. import com.vaadin.server.Responsive;
  27. import com.vaadin.shared.ui.Connect;
  28. import com.vaadin.shared.util.SharedUtil;
  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
  38. implements 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. /**
  62. * The theme that was in use when the width and height range caches were
  63. * created.
  64. */
  65. protected static String parsedTheme;
  66. private static Logger getLogger() {
  67. return Logger.getLogger(ResponsiveConnector.class.getName());
  68. }
  69. private static void error(String message) {
  70. getLogger().log(Level.SEVERE, message);
  71. }
  72. private static void warning(String message) {
  73. getLogger().warning(message);
  74. }
  75. @Override
  76. protected void extend(ServerConnector target) {
  77. this.target = (AbstractComponentConnector) target;
  78. // Start listening for size changes
  79. LayoutManager.get(getConnection()).addElementResizeListener(
  80. this.target.getWidget().getElement(), this);
  81. }
  82. /**
  83. * Construct the list of selectors that should be matched against in the
  84. * range selectors
  85. *
  86. * @return The selectors in a comma delimited string.
  87. */
  88. protected String constructSelectorsForTarget() {
  89. String primaryStyle = target.getState().primaryStyleName;
  90. StringBuilder selectors = new StringBuilder();
  91. selectors.append(".").append(primaryStyle);
  92. if (target.getState().styles != null
  93. && target.getState().styles.size() > 0) {
  94. for (String style : target.getState().styles) {
  95. selectors.append(",.").append(style);
  96. selectors.append(",.").append(primaryStyle).append(".")
  97. .append(style);
  98. selectors.append(",.").append(style).append(".")
  99. .append(primaryStyle);
  100. selectors.append(",.").append(primaryStyle).append("-")
  101. .append(style);
  102. }
  103. }
  104. // Allow the ID to be used as the selector as well for ranges
  105. if (target.getState().id != null) {
  106. selectors.append(",#").append(target.getState().id);
  107. }
  108. return selectors.toString();
  109. }
  110. @Override
  111. public void onUnregister() {
  112. super.onUnregister();
  113. LayoutManager.get(getConnection()).removeElementResizeListener(
  114. target.getWidget().getElement(), this);
  115. }
  116. @Override
  117. public void onStateChanged(StateChangeEvent event) {
  118. super.onStateChanged(event);
  119. // Changing the theme may introduce new style sheets so we may need to
  120. // rebuild the cache
  121. if (widthRangeCache == null
  122. || !SharedUtil.equals(parsedTheme, getCurrentThemeName())) {
  123. // updating break points
  124. searchForBreakPoints();
  125. }
  126. // Get any breakpoints from the styles defined for this widget
  127. getBreakPointsFor(constructSelectorsForTarget());
  128. // make sure that the ranges are updated at least once regardless of
  129. // resize events.
  130. updateRanges();
  131. }
  132. private String getCurrentThemeName() {
  133. return getConnection().getUIConnector().getActiveTheme();
  134. }
  135. private void searchForBreakPoints() {
  136. searchForBreakPointsNative();
  137. parsedTheme = getCurrentThemeName();
  138. }
  139. /**
  140. * Build a cache of all 'width-range' and 'height-range' attribute selectors
  141. * found in the stylesheets.
  142. */
  143. private static native void searchForBreakPointsNative()
  144. /*-{
  145. // Initialize variables
  146. @com.vaadin.client.extensions.ResponsiveConnector::widthRangeCache = [];
  147. @com.vaadin.client.extensions.ResponsiveConnector::heightRangeCache = [];
  148. var widthRanges = @com.vaadin.client.extensions.ResponsiveConnector::widthRangeCache;
  149. var heightRanges = @com.vaadin.client.extensions.ResponsiveConnector::heightRangeCache;
  150. // Can't do squat if we can't parse stylesheets
  151. if(!$doc.styleSheets)
  152. return;
  153. var sheets = $doc.styleSheets;
  154. // Loop all stylesheets on the page and process them individually
  155. for(var i = 0, len = sheets.length; i < len; i++) {
  156. var sheet = sheets[i];
  157. @com.vaadin.client.extensions.ResponsiveConnector::searchStylesheetForBreakPoints(Lcom/google/gwt/core/client/JavaScriptObject;)(sheet);
  158. }
  159. }-*/;
  160. /**
  161. * Process an individual stylesheet object. Any @import statements are
  162. * handled recursively. Regular rule declarations are searched for
  163. * 'width-range' and 'height-range' attribute selectors.
  164. *
  165. * @param sheet
  166. */
  167. private static native void searchStylesheetForBreakPoints(
  168. final JavaScriptObject sheet)
  169. /*-{
  170. // Inline variables for easier reading
  171. var widthRanges = @com.vaadin.client.extensions.ResponsiveConnector::widthRangeCache;
  172. var heightRanges = @com.vaadin.client.extensions.ResponsiveConnector::heightRangeCache;
  173. // Get all the rulesets from the stylesheet
  174. var theRules = new Array();
  175. var IEOrEdge = @com.vaadin.client.BrowserInfo::get()().@com.vaadin.client.BrowserInfo::isIE()() || @com.vaadin.client.BrowserInfo::get()().@com.vaadin.client.BrowserInfo::isEdge()();
  176. try {
  177. if (sheet.cssRules) {
  178. theRules = sheet.cssRules
  179. } else if (sheet.rules) {
  180. theRules = sheet.rules
  181. }
  182. } catch (e) {
  183. // FF spews if trying to access rules for cross domain styles
  184. @ResponsiveConnector::warning(*)("Can't process styles from " + sheet.href +
  185. ", probably because of cross domain issues: " + e);
  186. return;
  187. }
  188. // Loop through the rulesets
  189. for(var i = 0, len = theRules.length; i < len; i++) {
  190. var rule = theRules[i];
  191. if(rule.type == 3) {
  192. // @import rule, traverse recursively
  193. @com.vaadin.client.extensions.ResponsiveConnector::searchStylesheetForBreakPoints(Lcom/google/gwt/core/client/JavaScriptObject;)(rule.styleSheet);
  194. } else if(rule.type == 1 || !rule.type) {
  195. // Regular selector rule
  196. // Helper function
  197. var pushToCache = function(ranges, selector, min, max) {
  198. // Avoid adding duplicates
  199. var duplicate = false;
  200. for(var l = 0, len3 = ranges.length; l < len3; l++) {
  201. var bp = ranges[l];
  202. if (selector == bp[0] && min == bp[1] && max == bp[2]) {
  203. duplicate = true;
  204. break;
  205. }
  206. }
  207. if (!duplicate) {
  208. ranges.push([selector, min, max]);
  209. }
  210. };
  211. // Array of all of the separate selectors in this ruleset
  212. var haystack = rule.selectorText.split(",");
  213. // IE/Edge parses CSS like .class[attr="val"] into [attr="val"].class so we need to check for both
  214. var selectorRegEx = IEOrEdge ? /\[.*\]([\.|#]\S+)/ : /([\.|#]\S+?)\[.*\]/;
  215. // Loop all the selectors in this ruleset
  216. for(var k = 0, len2 = haystack.length; k < len2; k++) {
  217. // Split the haystack into parts.
  218. var widthRange = haystack[k].match(/\[width-range.*?\]/);
  219. var heightRange = haystack[k].match(/\[height-range.*?\]/);
  220. var selector = haystack[k].match(selectorRegEx);
  221. if (selector != null) {
  222. selector = selector[1];
  223. // Check for width-ranges.
  224. if (widthRange != null) {
  225. var minMax = widthRange[0].match(/\[width-range~?=["|'](.*?)-(.*?)["|']\]/i);
  226. var min = minMax[1];
  227. var max = minMax[2];
  228. pushToCache(widthRanges, selector, min, max);
  229. }
  230. // Check for height-ranges.
  231. if (heightRange != null) {
  232. var minMax = heightRange[0].match(/\[height-range~?=["|'](.*?)-(.*?)["|']\]/i);
  233. var min = minMax[1];
  234. var max = minMax[2];
  235. pushToCache(heightRanges, selector, min, max);
  236. }
  237. }
  238. }
  239. }
  240. }
  241. }-*/;
  242. /**
  243. * Get all matching ranges from the cache for this particular instance.
  244. *
  245. * @param selectors
  246. */
  247. private native void getBreakPointsFor(final String selectors)
  248. /*-{
  249. var selectors = selectors.split(",");
  250. var widthBreakpoints = this.@com.vaadin.client.extensions.ResponsiveConnector::widthBreakpoints = [];
  251. var heightBreakpoints = this.@com.vaadin.client.extensions.ResponsiveConnector::heightBreakpoints = [];
  252. var widthRanges = @com.vaadin.client.extensions.ResponsiveConnector::widthRangeCache;
  253. var heightRanges = @com.vaadin.client.extensions.ResponsiveConnector::heightRangeCache;
  254. for(var i = 0, len = widthRanges.length; i < len; i++) {
  255. var bp = widthRanges[i];
  256. for(var j = 0, len2 = selectors.length; j < len2; j++) {
  257. if(bp[0] == selectors[j])
  258. widthBreakpoints.push(bp);
  259. }
  260. }
  261. for(var i = 0, len = heightRanges.length; i < len; i++) {
  262. var bp = heightRanges[i];
  263. for(var j = 0, len2 = selectors.length; j < len2; j++) {
  264. if(bp[0] == selectors[j])
  265. heightBreakpoints.push(bp);
  266. }
  267. }
  268. // Only for debugging
  269. // console.log("Breakpoints for", selectors.join(","), widthBreakpoints, heightBreakpoints);
  270. }-*/;
  271. private String currentWidthRanges = "";
  272. private String currentHeightRanges = "";
  273. @Override
  274. public void onElementResize(final ElementResizeEvent event) {
  275. updateRanges();
  276. }
  277. private void updateRanges() {
  278. LayoutManager layoutManager = LayoutManager.get(getConnection());
  279. com.google.gwt.user.client.Element element = target.getWidget()
  280. .getElement();
  281. int width = layoutManager.getOuterWidth(element);
  282. int height = layoutManager.getOuterHeight(element);
  283. String oldWidthRanges = currentWidthRanges;
  284. String oldHeightRanges = currentHeightRanges;
  285. // Loop through breakpoints and see which one applies to this width
  286. currentWidthRanges = resolveBreakpoint("width", width);
  287. if (!"".equals(currentWidthRanges)) {
  288. element.setAttribute("width-range", currentWidthRanges);
  289. } else {
  290. element.removeAttribute("width-range");
  291. }
  292. // Loop through breakpoints and see which one applies to this height
  293. currentHeightRanges = resolveBreakpoint("height", height);
  294. if (!"".equals(currentHeightRanges)) {
  295. element.setAttribute("height-range", currentHeightRanges);
  296. } else {
  297. element.removeAttribute("height-range");
  298. }
  299. // If a new breakpoint is triggered, ensure all sizes are updated in
  300. // case some new styles are applied
  301. if (!currentWidthRanges.equals(oldWidthRanges)
  302. || !currentHeightRanges.equals(oldHeightRanges)) {
  303. layoutManager.setNeedsMeasureRecursively(
  304. ResponsiveConnector.this.target);
  305. }
  306. }
  307. private native String resolveBreakpoint(String which, int size)
  308. /*-{
  309. // Default to "width" breakpoints
  310. var breakpoints = this.@com.vaadin.client.extensions.ResponsiveConnector::widthBreakpoints;
  311. // Use height breakpoints if we're measuring the height
  312. if(which == "height")
  313. breakpoints = this.@com.vaadin.client.extensions.ResponsiveConnector::heightBreakpoints;
  314. // Output string that goes into either the "width-range" or "height-range" attribute in the element
  315. var ranges = "";
  316. // Loop the breakpoints
  317. for(var i = 0, len = breakpoints.length; i < len; i++) {
  318. var bp = breakpoints[i];
  319. var min = parseInt(bp[1]);
  320. var max = parseInt(bp[2]);
  321. if(!isNaN(min) && !isNaN(max)) {
  322. if(min <= size && size <= max) {
  323. ranges += " " + bp[1] + "-" + bp[2];
  324. }
  325. } else if (!isNaN(min)) {
  326. if(min <= size) {
  327. ranges += " " + bp[1] + "-";
  328. }
  329. } else if (!isNaN(max)) {
  330. if (size <= max) {
  331. ranges += " -" + bp[2];
  332. }
  333. }
  334. }
  335. // Trim the output and return it
  336. return ranges.replace(/^\s+/, "");
  337. }-*/;
  338. }