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

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