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.

VCustomLayout.java 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. /*
  2. @VaadinApache2LicenseForJavaFiles@
  3. */
  4. package com.vaadin.terminal.gwt.client.ui;
  5. import java.util.HashMap;
  6. import java.util.Iterator;
  7. import com.google.gwt.dom.client.ImageElement;
  8. import com.google.gwt.dom.client.NodeList;
  9. import com.google.gwt.user.client.DOM;
  10. import com.google.gwt.user.client.Element;
  11. import com.google.gwt.user.client.Event;
  12. import com.google.gwt.user.client.ui.ComplexPanel;
  13. import com.google.gwt.user.client.ui.Widget;
  14. import com.vaadin.terminal.gwt.client.ApplicationConnection;
  15. import com.vaadin.terminal.gwt.client.BrowserInfo;
  16. import com.vaadin.terminal.gwt.client.ComponentConnector;
  17. import com.vaadin.terminal.gwt.client.ConnectorMap;
  18. import com.vaadin.terminal.gwt.client.UIDL;
  19. import com.vaadin.terminal.gwt.client.Util;
  20. import com.vaadin.terminal.gwt.client.VCaption;
  21. import com.vaadin.terminal.gwt.client.VCaptionWrapper;
  22. /**
  23. * Custom Layout implements complex layout defined with HTML template.
  24. *
  25. * @author Vaadin Ltd
  26. *
  27. */
  28. public class VCustomLayout extends ComplexPanel {
  29. public static final String CLASSNAME = "v-customlayout";
  30. /** Location-name to containing element in DOM map */
  31. private final HashMap<String, Element> locationToElement = new HashMap<String, Element>();
  32. /** Location-name to contained widget map */
  33. final HashMap<String, Widget> locationToWidget = new HashMap<String, Widget>();
  34. /** Widget to captionwrapper map */
  35. private final HashMap<ComponentConnector, VCaptionWrapper> paintableToCaptionWrapper = new HashMap<ComponentConnector, VCaptionWrapper>();
  36. /** Name of the currently rendered style */
  37. String currentTemplateName;
  38. /** Unexecuted scripts loaded from the template */
  39. String scripts = "";
  40. /** Paintable ID of this paintable */
  41. String pid;
  42. ApplicationConnection client;
  43. /** Has the template been loaded from contents passed in UIDL **/
  44. private boolean hasTemplateContents = false;
  45. private Element elementWithNativeResizeFunction;
  46. private String height = "";
  47. private String width = "";
  48. public VCustomLayout() {
  49. setElement(DOM.createDiv());
  50. // Clear any unwanted styling
  51. DOM.setStyleAttribute(getElement(), "border", "none");
  52. DOM.setStyleAttribute(getElement(), "margin", "0");
  53. DOM.setStyleAttribute(getElement(), "padding", "0");
  54. if (BrowserInfo.get().isIE()) {
  55. DOM.setStyleAttribute(getElement(), "position", "relative");
  56. }
  57. setStyleName(CLASSNAME);
  58. }
  59. /**
  60. * Sets widget to given location.
  61. *
  62. * If location already contains a widget it will be removed.
  63. *
  64. * @param widget
  65. * Widget to be set into location.
  66. * @param location
  67. * location name where widget will be added
  68. *
  69. * @throws IllegalArgumentException
  70. * if no such location is found in the layout.
  71. */
  72. public void setWidget(Widget widget, String location) {
  73. if (widget == null) {
  74. return;
  75. }
  76. // If no given location is found in the layout, and exception is throws
  77. Element elem = locationToElement.get(location);
  78. if (elem == null && hasTemplate()) {
  79. throw new IllegalArgumentException("No location " + location
  80. + " found");
  81. }
  82. // Get previous widget
  83. final Widget previous = locationToWidget.get(location);
  84. // NOP if given widget already exists in this location
  85. if (previous == widget) {
  86. return;
  87. }
  88. if (previous != null) {
  89. remove(previous);
  90. }
  91. // if template is missing add element in order
  92. if (!hasTemplate()) {
  93. elem = getElement();
  94. }
  95. // Add widget to location
  96. super.add(widget, elem);
  97. locationToWidget.put(location, widget);
  98. }
  99. /** Initialize HTML-layout. */
  100. void initializeHTML(UIDL uidl, ApplicationConnection client) {
  101. final String newTemplateContents = uidl
  102. .getStringAttribute("templateContents");
  103. final String newTemplate = uidl.getStringAttribute("template");
  104. currentTemplateName = null;
  105. hasTemplateContents = false;
  106. String template = "";
  107. if (newTemplate != null) {
  108. // Get the HTML-template from client
  109. template = client.getResource("layouts/" + newTemplate + ".html");
  110. if (template == null) {
  111. template = "<em>Layout file layouts/"
  112. + newTemplate
  113. + ".html is missing. Components will be drawn for debug purposes.</em>";
  114. } else {
  115. currentTemplateName = newTemplate;
  116. }
  117. } else {
  118. hasTemplateContents = true;
  119. template = newTemplateContents;
  120. }
  121. // Connect body of the template to DOM
  122. template = extractBodyAndScriptsFromTemplate(template);
  123. // TODO prefix img src:s here with a regeps, cannot work further with IE
  124. String themeUri = client.getThemeUri();
  125. String relImgPrefix = themeUri + "/layouts/";
  126. // prefix all relative image elements to point to theme dir with a
  127. // regexp search
  128. template = template.replaceAll(
  129. "<((?:img)|(?:IMG))\\s([^>]*)src=\"((?![a-z]+:)[^/][^\"]+)\"",
  130. "<$1 $2src=\"" + relImgPrefix + "$3\"");
  131. // also support src attributes without quotes
  132. template = template
  133. .replaceAll(
  134. "<((?:img)|(?:IMG))\\s([^>]*)src=[^\"]((?![a-z]+:)[^/][^ />]+)[ />]",
  135. "<$1 $2src=\"" + relImgPrefix + "$3\"");
  136. // also prefix relative style="...url(...)..."
  137. template = template
  138. .replaceAll(
  139. "(<[^>]+style=\"[^\"]*url\\()((?![a-z]+:)[^/][^\"]+)(\\)[^>]*>)",
  140. "$1 " + relImgPrefix + "$2 $3");
  141. getElement().setInnerHTML(template);
  142. // Remap locations to elements
  143. locationToElement.clear();
  144. scanForLocations(getElement());
  145. initImgElements();
  146. elementWithNativeResizeFunction = DOM.getFirstChild(getElement());
  147. if (elementWithNativeResizeFunction == null) {
  148. elementWithNativeResizeFunction = getElement();
  149. }
  150. publishResizedFunction(elementWithNativeResizeFunction);
  151. }
  152. private native boolean uriEndsWithSlash()
  153. /*-{
  154. var path = $wnd.location.pathname;
  155. if(path.charAt(path.length - 1) == "/")
  156. return true;
  157. return false;
  158. }-*/;
  159. boolean hasTemplate() {
  160. if (currentTemplateName == null && !hasTemplateContents) {
  161. return false;
  162. } else {
  163. return true;
  164. }
  165. }
  166. /** Collect locations from template */
  167. private void scanForLocations(Element elem) {
  168. final String location = elem.getAttribute("location");
  169. if (!"".equals(location)) {
  170. locationToElement.put(location, elem);
  171. elem.setInnerHTML("");
  172. } else {
  173. final int len = DOM.getChildCount(elem);
  174. for (int i = 0; i < len; i++) {
  175. scanForLocations(DOM.getChild(elem, i));
  176. }
  177. }
  178. }
  179. /** Evaluate given script in browser document */
  180. static native void eval(String script)
  181. /*-{
  182. try {
  183. if (script != null)
  184. eval("{ var document = $doc; var window = $wnd; "+ script + "}");
  185. } catch (e) {
  186. }
  187. }-*/;
  188. /**
  189. * Img elements needs some special handling in custom layout. Img elements
  190. * will get their onload events sunk. This way custom layout can notify
  191. * parent about possible size change.
  192. */
  193. private void initImgElements() {
  194. NodeList<com.google.gwt.dom.client.Element> nodeList = getElement()
  195. .getElementsByTagName("IMG");
  196. for (int i = 0; i < nodeList.getLength(); i++) {
  197. com.google.gwt.dom.client.ImageElement img = (ImageElement) nodeList
  198. .getItem(i);
  199. DOM.sinkEvents((Element) img.cast(), Event.ONLOAD);
  200. }
  201. }
  202. /**
  203. * Extract body part and script tags from raw html-template.
  204. *
  205. * Saves contents of all script-tags to private property: scripts. Returns
  206. * contents of the body part for the html without script-tags. Also replaces
  207. * all _UID_ tags with an unique id-string.
  208. *
  209. * @param html
  210. * Original HTML-template received from server
  211. * @return html that is used to create the HTMLPanel.
  212. */
  213. private String extractBodyAndScriptsFromTemplate(String html) {
  214. // Replace UID:s
  215. html = html.replaceAll("_UID_", pid + "__");
  216. // Exctract script-tags
  217. scripts = "";
  218. int endOfPrevScript = 0;
  219. int nextPosToCheck = 0;
  220. String lc = html.toLowerCase();
  221. String res = "";
  222. int scriptStart = lc.indexOf("<script", nextPosToCheck);
  223. while (scriptStart > 0) {
  224. res += html.substring(endOfPrevScript, scriptStart);
  225. scriptStart = lc.indexOf(">", scriptStart);
  226. final int j = lc.indexOf("</script>", scriptStart);
  227. scripts += html.substring(scriptStart + 1, j) + ";";
  228. nextPosToCheck = endOfPrevScript = j + "</script>".length();
  229. scriptStart = lc.indexOf("<script", nextPosToCheck);
  230. }
  231. res += html.substring(endOfPrevScript);
  232. // Extract body
  233. html = res;
  234. lc = html.toLowerCase();
  235. int startOfBody = lc.indexOf("<body");
  236. if (startOfBody < 0) {
  237. res = html;
  238. } else {
  239. res = "";
  240. startOfBody = lc.indexOf(">", startOfBody) + 1;
  241. final int endOfBody = lc.indexOf("</body>", startOfBody);
  242. if (endOfBody > startOfBody) {
  243. res = html.substring(startOfBody, endOfBody);
  244. } else {
  245. res = html.substring(startOfBody);
  246. }
  247. }
  248. return res;
  249. }
  250. /** Update caption for given widget */
  251. public void updateCaption(ComponentConnector paintable, UIDL uidl) {
  252. VCaptionWrapper wrapper = paintableToCaptionWrapper.get(paintable);
  253. Widget widget = paintable.getWidget();
  254. if (VCaption.isNeeded(uidl, paintable.getState())) {
  255. if (wrapper == null) {
  256. // Add a wrapper between the layout and the child widget
  257. final String loc = getLocation(widget);
  258. super.remove(widget);
  259. wrapper = new VCaptionWrapper(paintable, client);
  260. super.add(wrapper, locationToElement.get(loc));
  261. paintableToCaptionWrapper.put(paintable, wrapper);
  262. }
  263. wrapper.updateCaption(uidl);
  264. } else {
  265. if (wrapper != null) {
  266. // Remove the wrapper and add the widget directly to the layout
  267. final String loc = getLocation(widget);
  268. super.remove(wrapper);
  269. super.add(widget, locationToElement.get(loc));
  270. paintableToCaptionWrapper.remove(paintable);
  271. }
  272. }
  273. }
  274. /** Get the location of an widget */
  275. public String getLocation(Widget w) {
  276. for (final Iterator<String> i = locationToWidget.keySet().iterator(); i
  277. .hasNext();) {
  278. final String location = i.next();
  279. if (locationToWidget.get(location) == w) {
  280. return location;
  281. }
  282. }
  283. return null;
  284. }
  285. /** Removes given widget from the layout */
  286. @Override
  287. public boolean remove(Widget w) {
  288. ComponentConnector paintable = ConnectorMap.get(client).getConnector(w);
  289. client.unregisterPaintable(paintable);
  290. final String location = getLocation(w);
  291. if (location != null) {
  292. locationToWidget.remove(location);
  293. }
  294. final VCaptionWrapper cw = paintableToCaptionWrapper.get(paintable);
  295. if (cw != null) {
  296. paintableToCaptionWrapper.remove(paintable);
  297. return super.remove(cw);
  298. } else if (w != null) {
  299. return super.remove(w);
  300. }
  301. return false;
  302. }
  303. /** Adding widget without specifying location is not supported */
  304. @Override
  305. public void add(Widget w) {
  306. throw new UnsupportedOperationException();
  307. }
  308. /** Clear all widgets from the layout */
  309. @Override
  310. public void clear() {
  311. super.clear();
  312. locationToWidget.clear();
  313. paintableToCaptionWrapper.clear();
  314. }
  315. /**
  316. * This method is published to JS side with the same name into first DOM
  317. * node of custom layout. This way if one implements some resizeable
  318. * containers in custom layout he/she can notify children after resize.
  319. */
  320. public void notifyChildrenOfSizeChange() {
  321. client.runDescendentsLayout(this);
  322. }
  323. @Override
  324. public void onDetach() {
  325. super.onDetach();
  326. if (elementWithNativeResizeFunction != null) {
  327. detachResizedFunction(elementWithNativeResizeFunction);
  328. }
  329. }
  330. private native void detachResizedFunction(Element element)
  331. /*-{
  332. element.notifyChildrenOfSizeChange = null;
  333. }-*/;
  334. private native void publishResizedFunction(Element element)
  335. /*-{
  336. var self = this;
  337. element.notifyChildrenOfSizeChange = function() {
  338. self.@com.vaadin.terminal.gwt.client.ui.VCustomLayout::notifyChildrenOfSizeChange()();
  339. };
  340. }-*/;
  341. /**
  342. * In custom layout one may want to run layout functions made with
  343. * JavaScript. This function tests if one exists (with name "iLayoutJS" in
  344. * layouts first DOM node) and runs et. Return value is used to determine if
  345. * children needs to be notified of size changes.
  346. *
  347. * Note! When implementing a JS layout function you most likely want to call
  348. * notifyChildrenOfSizeChange() function on your custom layouts main
  349. * element. That method is used to control whether child components layout
  350. * functions are to be run.
  351. *
  352. * @param el
  353. * @return true if layout function exists and was run successfully, else
  354. * false.
  355. */
  356. native boolean iLayoutJS(Element el)
  357. /*-{
  358. if(el && el.iLayoutJS) {
  359. try {
  360. el.iLayoutJS();
  361. return true;
  362. } catch (e) {
  363. return false;
  364. }
  365. } else {
  366. return false;
  367. }
  368. }-*/;
  369. @Override
  370. public void onBrowserEvent(Event event) {
  371. super.onBrowserEvent(event);
  372. if (event.getTypeInt() == Event.ONLOAD) {
  373. Util.notifyParentOfSizeChange(this, true);
  374. event.cancelBubble(true);
  375. }
  376. }
  377. }