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.

VFilterSelect.java 37KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059
  1. /*
  2. @ITMillApache2LicenseForJavaFiles@
  3. */
  4. package com.vaadin.terminal.gwt.client.ui;
  5. import java.util.ArrayList;
  6. import java.util.Collection;
  7. import java.util.Date;
  8. import java.util.Iterator;
  9. import java.util.List;
  10. import com.google.gwt.user.client.Command;
  11. import com.google.gwt.user.client.DOM;
  12. import com.google.gwt.user.client.Element;
  13. import com.google.gwt.user.client.Event;
  14. import com.google.gwt.user.client.Window;
  15. import com.google.gwt.user.client.ui.ClickListener;
  16. import com.google.gwt.user.client.ui.Composite;
  17. import com.google.gwt.user.client.ui.FlowPanel;
  18. import com.google.gwt.user.client.ui.FocusListener;
  19. import com.google.gwt.user.client.ui.HTML;
  20. import com.google.gwt.user.client.ui.Image;
  21. import com.google.gwt.user.client.ui.KeyboardListener;
  22. import com.google.gwt.user.client.ui.LoadListener;
  23. import com.google.gwt.user.client.ui.PopupListener;
  24. import com.google.gwt.user.client.ui.PopupPanel;
  25. import com.google.gwt.user.client.ui.TextBox;
  26. import com.google.gwt.user.client.ui.Widget;
  27. import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
  28. import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
  29. import com.vaadin.terminal.gwt.client.ApplicationConnection;
  30. import com.vaadin.terminal.gwt.client.BrowserInfo;
  31. import com.vaadin.terminal.gwt.client.Focusable;
  32. import com.vaadin.terminal.gwt.client.VTooltip;
  33. import com.vaadin.terminal.gwt.client.Paintable;
  34. import com.vaadin.terminal.gwt.client.UIDL;
  35. import com.vaadin.terminal.gwt.client.Util;
  36. /**
  37. *
  38. * TODO needs major refactoring (to be extensible etc)
  39. */
  40. public class VFilterSelect extends Composite implements Paintable, Field,
  41. KeyboardListener, ClickListener, FocusListener, Focusable {
  42. public class FilterSelectSuggestion implements Suggestion, Command {
  43. private final String key;
  44. private final String caption;
  45. private String iconUri;
  46. public FilterSelectSuggestion(UIDL uidl) {
  47. key = uidl.getStringAttribute("key");
  48. caption = uidl.getStringAttribute("caption");
  49. if (uidl.hasAttribute("icon")) {
  50. iconUri = client.translateToolkitUri(uidl
  51. .getStringAttribute("icon"));
  52. }
  53. }
  54. public String getDisplayString() {
  55. final StringBuffer sb = new StringBuffer();
  56. if (iconUri != null) {
  57. sb.append("<img src=\"");
  58. sb.append(iconUri);
  59. sb.append("\" alt=\"\" class=\"i-icon\" />");
  60. }
  61. sb.append("<span>" + Util.escapeHTML(caption) + "</span>");
  62. return sb.toString();
  63. }
  64. public String getReplacementString() {
  65. return caption;
  66. }
  67. public int getOptionKey() {
  68. return Integer.parseInt(key);
  69. }
  70. public String getIconUri() {
  71. return iconUri;
  72. }
  73. public void execute() {
  74. onSuggestionSelected(this);
  75. }
  76. }
  77. public class SuggestionPopup extends VToolkitOverlay implements
  78. PositionCallback, PopupListener {
  79. private static final String Z_INDEX = "30000";
  80. private final SuggestionMenu menu;
  81. private final Element up = DOM.createDiv();
  82. private final Element down = DOM.createDiv();
  83. private final Element status = DOM.createDiv();
  84. private boolean isPagingEnabled = true;
  85. private long lastAutoClosed;
  86. private int popupOuterPadding = -1;
  87. private int topPosition;
  88. SuggestionPopup() {
  89. super(true, false, true);
  90. menu = new SuggestionMenu();
  91. setWidget(menu);
  92. setStyleName(CLASSNAME + "-suggestpopup");
  93. DOM.setStyleAttribute(getElement(), "zIndex", Z_INDEX);
  94. final Element root = getContainerElement();
  95. DOM.setInnerHTML(up, "<span>Prev</span>");
  96. DOM.sinkEvents(up, Event.ONCLICK);
  97. DOM.setInnerHTML(down, "<span>Next</span>");
  98. DOM.sinkEvents(down, Event.ONCLICK);
  99. DOM.insertChild(root, up, 0);
  100. DOM.appendChild(root, down);
  101. DOM.appendChild(root, status);
  102. DOM.setElementProperty(status, "className", CLASSNAME + "-status");
  103. addPopupListener(this);
  104. }
  105. public void showSuggestions(
  106. Collection<FilterSelectSuggestion> currentSuggestions,
  107. int currentPage, int totalSuggestions) {
  108. // Add TT anchor point
  109. DOM.setElementProperty(getElement(), "id",
  110. "TOOLKIT_COMBOBOX_OPTIONLIST");
  111. menu.setSuggestions(currentSuggestions);
  112. final int x = VFilterSelect.this.getAbsoluteLeft();
  113. topPosition = tb.getAbsoluteTop();
  114. topPosition += tb.getOffsetHeight();
  115. setPopupPosition(x, topPosition);
  116. final int first = currentPage * PAGELENTH
  117. + (nullSelectionAllowed && currentPage > 0 ? 0 : 1);
  118. final int last = first + currentSuggestions.size() - 1;
  119. final int matches = totalSuggestions
  120. - (nullSelectionAllowed ? 1 : 0);
  121. if (last > 0) {
  122. // nullsel not counted, as requested by user
  123. DOM.setInnerText(status, (matches == 0 ? 0 : first)
  124. + "-"
  125. + ("".equals(lastFilter) && nullSelectionAllowed
  126. && currentPage == 0 ? last - 1 : last) + "/"
  127. + matches);
  128. } else {
  129. DOM.setInnerText(status, "");
  130. }
  131. // We don't need to show arrows or statusbar if there is only one
  132. // page
  133. if (matches <= PAGELENTH) {
  134. setPagingEnabled(false);
  135. } else {
  136. setPagingEnabled(true);
  137. }
  138. setPrevButtonActive(first > 1);
  139. setNextButtonActive(last < matches);
  140. // clear previously fixed width
  141. menu.setWidth("");
  142. DOM.setStyleAttribute(DOM.getFirstChild(menu.getElement()),
  143. "width", "");
  144. setPopupPositionAndShow(this);
  145. }
  146. private void setNextButtonActive(boolean b) {
  147. if (b) {
  148. DOM.sinkEvents(down, Event.ONCLICK);
  149. DOM.setElementProperty(down, "className", CLASSNAME
  150. + "-nextpage");
  151. } else {
  152. DOM.sinkEvents(down, 0);
  153. DOM.setElementProperty(down, "className", CLASSNAME
  154. + "-nextpage-off");
  155. }
  156. }
  157. private void setPrevButtonActive(boolean b) {
  158. if (b) {
  159. DOM.sinkEvents(up, Event.ONCLICK);
  160. DOM
  161. .setElementProperty(up, "className", CLASSNAME
  162. + "-prevpage");
  163. } else {
  164. DOM.sinkEvents(up, 0);
  165. DOM.setElementProperty(up, "className", CLASSNAME
  166. + "-prevpage-off");
  167. }
  168. }
  169. public void selectNextItem() {
  170. final MenuItem cur = menu.getSelectedItem();
  171. final int index = 1 + menu.getItems().indexOf(cur);
  172. if (menu.getItems().size() > index) {
  173. final MenuItem newSelectedItem = (MenuItem) menu.getItems()
  174. .get(index);
  175. menu.selectItem(newSelectedItem);
  176. tb.setText(newSelectedItem.getText());
  177. tb.setSelectionRange(lastFilter.length(), newSelectedItem
  178. .getText().length()
  179. - lastFilter.length());
  180. } else if (hasNextPage()) {
  181. lastIndex = index - 1; // save for paging
  182. filterOptions(currentPage + 1, lastFilter);
  183. }
  184. }
  185. public void selectPrevItem() {
  186. final MenuItem cur = menu.getSelectedItem();
  187. final int index = -1 + menu.getItems().indexOf(cur);
  188. if (index > -1) {
  189. final MenuItem newSelectedItem = (MenuItem) menu.getItems()
  190. .get(index);
  191. menu.selectItem(newSelectedItem);
  192. tb.setText(newSelectedItem.getText());
  193. tb.setSelectionRange(lastFilter.length(), newSelectedItem
  194. .getText().length()
  195. - lastFilter.length());
  196. } else if (index == -1) {
  197. if (currentPage > 0) {
  198. lastIndex = index + 1; // save for paging
  199. filterOptions(currentPage - 1, lastFilter);
  200. }
  201. } else {
  202. final MenuItem newSelectedItem = (MenuItem) menu.getItems()
  203. .get(menu.getItems().size() - 1);
  204. menu.selectItem(newSelectedItem);
  205. tb.setText(newSelectedItem.getText());
  206. tb.setSelectionRange(lastFilter.length(), newSelectedItem
  207. .getText().length()
  208. - lastFilter.length());
  209. }
  210. }
  211. @Override
  212. public void onBrowserEvent(Event event) {
  213. final Element target = DOM.eventGetTarget(event);
  214. if (DOM.compare(target, up)
  215. || DOM.compare(target, DOM.getChild(up, 0))) {
  216. filterOptions(currentPage - 1, lastFilter);
  217. } else if (DOM.compare(target, down)
  218. || DOM.compare(target, DOM.getChild(down, 0))) {
  219. filterOptions(currentPage + 1, lastFilter);
  220. }
  221. tb.setFocus(true);
  222. }
  223. public void setPagingEnabled(boolean paging) {
  224. if (isPagingEnabled == paging) {
  225. return;
  226. }
  227. if (paging) {
  228. DOM.setStyleAttribute(down, "display", "");
  229. DOM.setStyleAttribute(up, "display", "");
  230. DOM.setStyleAttribute(status, "display", "");
  231. } else {
  232. DOM.setStyleAttribute(down, "display", "none");
  233. DOM.setStyleAttribute(up, "display", "none");
  234. DOM.setStyleAttribute(status, "display", "none");
  235. }
  236. isPagingEnabled = paging;
  237. }
  238. /*
  239. * (non-Javadoc)
  240. *
  241. * @see
  242. * com.google.gwt.user.client.ui.PopupPanel$PositionCallback#setPosition
  243. * (int, int)
  244. */
  245. public void setPosition(int offsetWidth, int offsetHeight) {
  246. int top = -1;
  247. int left = -1;
  248. // reset menu size and retrieve its "natural" size
  249. menu.setHeight("");
  250. if (currentPage > 0) {
  251. // fix height to avoid height change when getting to last page
  252. menu.fixHeightTo(PAGELENTH);
  253. }
  254. offsetHeight = getOffsetHeight();
  255. final int desiredWidth = getMainWidth();
  256. int naturalMenuWidth = DOM.getElementPropertyInt(DOM
  257. .getFirstChild(menu.getElement()), "offsetWidth");
  258. if (popupOuterPadding == -1) {
  259. popupOuterPadding = Util.measureHorizontalPaddingAndBorder(
  260. getElement(), 2);
  261. }
  262. if (naturalMenuWidth < desiredWidth) {
  263. menu.setWidth((desiredWidth - popupOuterPadding) + "px");
  264. DOM.setStyleAttribute(DOM.getFirstChild(menu.getElement()),
  265. "width", "100%");
  266. naturalMenuWidth = desiredWidth;
  267. }
  268. if (BrowserInfo.get().isIE()) {
  269. /*
  270. * IE requires us to specify the width for the container
  271. * element. Otherwise it will be 100% wide
  272. */
  273. int rootWidth = naturalMenuWidth - popupOuterPadding;
  274. DOM.setStyleAttribute(getContainerElement(), "width", rootWidth
  275. + "px");
  276. }
  277. if (offsetHeight + getPopupTop() > Window.getClientHeight()
  278. + Window.getScrollTop()) {
  279. // popup on top of input instead
  280. top = getPopupTop() - offsetHeight
  281. - VFilterSelect.this.getOffsetHeight();
  282. if (top < 0) {
  283. top = 0;
  284. }
  285. } else {
  286. top = getPopupTop();
  287. /*
  288. * Take popup top margin into account. getPopupTop() returns the
  289. * top value including the margin but the value we give must not
  290. * include the margin.
  291. */
  292. int topMargin = (top - topPosition);
  293. top -= topMargin;
  294. }
  295. // fetch real width (mac FF bugs here due GWT popups overflow:auto )
  296. offsetWidth = DOM.getElementPropertyInt(DOM.getFirstChild(menu
  297. .getElement()), "offsetWidth");
  298. if (offsetWidth + getPopupLeft() > Window.getClientWidth()
  299. + Window.getScrollLeft()) {
  300. left = VFilterSelect.this.getAbsoluteLeft()
  301. + VFilterSelect.this.getOffsetWidth()
  302. + Window.getScrollLeft() - offsetWidth;
  303. if (left < 0) {
  304. left = 0;
  305. }
  306. } else {
  307. left = getPopupLeft();
  308. }
  309. setPopupPosition(left, top);
  310. }
  311. /**
  312. * @return true if popup was just closed
  313. */
  314. public boolean isJustClosed() {
  315. final long now = (new Date()).getTime();
  316. return (lastAutoClosed > 0 && (now - lastAutoClosed) < 200);
  317. }
  318. public void onPopupClosed(PopupPanel sender, boolean autoClosed) {
  319. if (autoClosed) {
  320. lastAutoClosed = (new Date()).getTime();
  321. }
  322. }
  323. /**
  324. * Updates style names in suggestion popup to help theme building.
  325. */
  326. public void updateStyleNames(UIDL uidl) {
  327. if (uidl.hasAttribute("style")) {
  328. setStyleName(CLASSNAME + "-suggestpopup");
  329. final String[] styles = uidl.getStringAttribute("style").split(
  330. " ");
  331. for (int i = 0; i < styles.length; i++) {
  332. addStyleDependentName(styles[i]);
  333. }
  334. }
  335. }
  336. }
  337. public class SuggestionMenu extends MenuBar {
  338. SuggestionMenu() {
  339. super(true);
  340. setStyleName(CLASSNAME + "-suggestmenu");
  341. }
  342. /**
  343. * Fixes menus height to use same space as full page would use. Needed
  344. * to avoid height changes when quickly "scrolling" to last page
  345. */
  346. public void fixHeightTo(int pagelenth) {
  347. if (currentSuggestions.size() > 0) {
  348. final int pixels = pagelenth * (getOffsetHeight() - 2)
  349. / currentSuggestions.size();
  350. setHeight((pixels + 2) + "px");
  351. }
  352. }
  353. public void setSuggestions(
  354. Collection<FilterSelectSuggestion> suggestions) {
  355. clearItems();
  356. final Iterator<FilterSelectSuggestion> it = suggestions.iterator();
  357. while (it.hasNext()) {
  358. final FilterSelectSuggestion s = it.next();
  359. final MenuItem mi = new MenuItem(s.getDisplayString(), true, s);
  360. com.google.gwt.dom.client.Element child = mi.getElement()
  361. .getFirstChildElement();
  362. while (child != null) {
  363. if (child.getNodeName().toLowerCase().equals("img")) {
  364. DOM
  365. .sinkEvents((Element) child.cast(),
  366. (DOM.getEventsSunk((Element) child
  367. .cast()) | Event.ONLOAD));
  368. }
  369. child = child.getNextSiblingElement();
  370. }
  371. this.addItem(mi);
  372. if (s == currentSuggestion) {
  373. selectItem(mi);
  374. }
  375. }
  376. }
  377. public void doSelectedItemAction() {
  378. final MenuItem item = getSelectedItem();
  379. final String enteredItemValue = tb.getText();
  380. // check for exact match in menu
  381. int p = getItems().size();
  382. if (p > 0) {
  383. for (int i = 0; i < p; i++) {
  384. final MenuItem potentialExactMatch = (MenuItem) getItems()
  385. .get(i);
  386. if (potentialExactMatch.getText().equals(enteredItemValue)) {
  387. selectItem(potentialExactMatch);
  388. doItemAction(potentialExactMatch, true);
  389. suggestionPopup.hide();
  390. return;
  391. }
  392. }
  393. }
  394. if (allowNewItem) {
  395. if (!prompting && !enteredItemValue.equals(lastNewItemString)) {
  396. /*
  397. * Store last sent new item string to avoid double sends
  398. */
  399. lastNewItemString = enteredItemValue;
  400. client.updateVariable(paintableId, "newitem",
  401. enteredItemValue, immediate);
  402. }
  403. } else if (item != null
  404. && !"".equals(lastFilter)
  405. && item.getText().toLowerCase().startsWith(
  406. lastFilter.toLowerCase())) {
  407. doItemAction(item, true);
  408. } else {
  409. if (currentSuggestion != null) {
  410. String text = currentSuggestion.getReplacementString();
  411. /* TODO?
  412. if (text.equals("")) {
  413. addStyleDependentName(CLASSNAME_PROMPT);
  414. tb.setText(inputPrompt);
  415. prompting = true;
  416. } else {
  417. tb.setText(text);
  418. prompting = false;
  419. removeStyleDependentName(CLASSNAME_PROMPT);
  420. }
  421. */
  422. selectedOptionKey = currentSuggestion.key;
  423. }
  424. }
  425. suggestionPopup.hide();
  426. }
  427. @Override
  428. public void onBrowserEvent(Event event) {
  429. if (event.getTypeInt() == Event.ONLOAD) {
  430. if (suggestionPopup.isVisible()) {
  431. setWidth("");
  432. DOM.setStyleAttribute(DOM.getFirstChild(getElement()),
  433. "width", "");
  434. suggestionPopup.setPopupPositionAndShow(suggestionPopup);
  435. }
  436. }
  437. super.onBrowserEvent(event);
  438. }
  439. }
  440. public static final int FILTERINGMODE_OFF = 0;
  441. public static final int FILTERINGMODE_STARTSWITH = 1;
  442. public static final int FILTERINGMODE_CONTAINS = 2;
  443. private static final String CLASSNAME = "i-filterselect";
  444. public static final int PAGELENTH = 10;
  445. private final FlowPanel panel = new FlowPanel();
  446. private final TextBox tb = new TextBox() {
  447. @Override
  448. public void onBrowserEvent(Event event) {
  449. super.onBrowserEvent(event);
  450. if (client != null) {
  451. client.handleTooltipEvent(event, VFilterSelect.this);
  452. }
  453. }
  454. };
  455. private final SuggestionPopup suggestionPopup = new SuggestionPopup();
  456. private final HTML popupOpener = new HTML("");
  457. private final Image selectedItemIcon = new Image();
  458. private ApplicationConnection client;
  459. private String paintableId;
  460. private int currentPage;
  461. private final Collection<FilterSelectSuggestion> currentSuggestions = new ArrayList<FilterSelectSuggestion>();
  462. private boolean immediate;
  463. private String selectedOptionKey;
  464. private boolean filtering = false;
  465. private String lastFilter = "";
  466. private int lastIndex = -1; // last selected index when using arrows
  467. private FilterSelectSuggestion currentSuggestion;
  468. private int totalMatches;
  469. private boolean allowNewItem;
  470. private boolean nullSelectionAllowed;
  471. private boolean enabled;
  472. // shown in unfocused empty field, disappears on focus (e.g "Search here")
  473. private static final String CLASSNAME_PROMPT = "prompt";
  474. private static final String ATTR_INPUTPROMPT = "prompt";
  475. private String inputPrompt = "";
  476. private boolean prompting = false;
  477. // Set true when popupopened has been clicked. Cleared on each UIDL-update.
  478. // This handles the special case where are not filtering yet and the
  479. // selected value has changed on the server-side. See #2119
  480. private boolean popupOpenerClicked;
  481. private String width = null;
  482. private int textboxPadding = -1;
  483. private int componentPadding = -1;
  484. private int suggestionPopupMinWidth = 0;
  485. /*
  486. * Stores the last new item string to avoid double submissions. Cleared on
  487. * uidl updates
  488. */
  489. private String lastNewItemString;
  490. private boolean focused = false;
  491. public VFilterSelect() {
  492. selectedItemIcon.setVisible(false);
  493. selectedItemIcon.setStyleName("i-icon");
  494. selectedItemIcon.addLoadListener(new LoadListener() {
  495. public void onError(Widget sender) {
  496. }
  497. public void onLoad(Widget sender) {
  498. updateRootWidth();
  499. updateSelectedIconPosition();
  500. }
  501. });
  502. panel.add(selectedItemIcon);
  503. tb.sinkEvents(VTooltip.TOOLTIP_EVENTS);
  504. panel.add(tb);
  505. panel.add(popupOpener);
  506. initWidget(panel);
  507. setStyleName(CLASSNAME);
  508. tb.addKeyboardListener(this);
  509. tb.setStyleName(CLASSNAME + "-input");
  510. tb.addFocusListener(this);
  511. popupOpener.setStyleName(CLASSNAME + "-button");
  512. popupOpener.addClickListener(this);
  513. }
  514. public boolean hasNextPage() {
  515. if (totalMatches > (currentPage + 1) * PAGELENTH) {
  516. return true;
  517. } else {
  518. return false;
  519. }
  520. }
  521. public void filterOptions(int page) {
  522. filterOptions(page, tb.getText());
  523. }
  524. public void filterOptions(int page, String filter) {
  525. if (filter.equals(lastFilter) && currentPage == page) {
  526. if (!suggestionPopup.isAttached()) {
  527. suggestionPopup.showSuggestions(currentSuggestions,
  528. currentPage, totalMatches);
  529. }
  530. return;
  531. }
  532. if (!filter.equals(lastFilter)) {
  533. // we are on subsequent page and text has changed -> reset page
  534. if ("".equals(filter)) {
  535. // let server decide
  536. page = -1;
  537. } else {
  538. page = 0;
  539. }
  540. }
  541. filtering = true;
  542. client.updateVariable(paintableId, "filter", filter, false);
  543. client.updateVariable(paintableId, "page", page, true);
  544. lastFilter = filter;
  545. currentPage = page;
  546. }
  547. public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
  548. paintableId = uidl.getId();
  549. this.client = client;
  550. boolean readonly = uidl.hasAttribute("readonly");
  551. boolean disabled = uidl.hasAttribute("disabled");
  552. if (disabled || readonly) {
  553. tb.setEnabled(false);
  554. enabled = false;
  555. } else {
  556. tb.setEnabled(true);
  557. enabled = true;
  558. }
  559. if (client.updateComponent(this, uidl, true)) {
  560. return;
  561. }
  562. // not a FocusWidget -> needs own tabindex handling
  563. if (uidl.hasAttribute("tabindex")) {
  564. tb.setTabIndex(uidl.getIntAttribute("tabindex"));
  565. }
  566. immediate = uidl.hasAttribute("immediate");
  567. nullSelectionAllowed = uidl.hasAttribute("nullselect");
  568. currentPage = uidl.getIntVariable("page");
  569. if (uidl.hasAttribute(ATTR_INPUTPROMPT)) {
  570. // input prompt changed from server
  571. inputPrompt = uidl.getStringAttribute(ATTR_INPUTPROMPT);
  572. } else {
  573. inputPrompt = "";
  574. }
  575. suggestionPopup.setPagingEnabled(true);
  576. suggestionPopup.updateStyleNames(uidl);
  577. allowNewItem = uidl.hasAttribute("allownewitem");
  578. lastNewItemString = null;
  579. currentSuggestions.clear();
  580. final UIDL options = uidl.getChildUIDL(0);
  581. totalMatches = uidl.getIntAttribute("totalMatches");
  582. String captions = inputPrompt;
  583. for (final Iterator i = options.getChildIterator(); i.hasNext();) {
  584. final UIDL optionUidl = (UIDL) i.next();
  585. final FilterSelectSuggestion suggestion = new FilterSelectSuggestion(
  586. optionUidl);
  587. currentSuggestions.add(suggestion);
  588. if (optionUidl.hasAttribute("selected")) {
  589. if (!filtering || popupOpenerClicked) {
  590. tb.setText(suggestion.getReplacementString());
  591. selectedOptionKey = "" + suggestion.getOptionKey();
  592. }
  593. currentSuggestion = suggestion;
  594. setSelectedItemIcon(suggestion.getIconUri());
  595. }
  596. // Collect captions so we can calculate minimum width for textarea
  597. if (captions.length() > 0) {
  598. captions += "|";
  599. }
  600. captions += suggestion.getReplacementString();
  601. }
  602. if ((!filtering || popupOpenerClicked) && uidl.hasVariable("selected")
  603. && uidl.getStringArrayVariable("selected").length == 0) {
  604. // select nulled
  605. if (!filtering || !popupOpenerClicked) {
  606. setPromptingOn();
  607. }
  608. selectedOptionKey = null;
  609. }
  610. if (filtering
  611. && lastFilter.toLowerCase().equals(
  612. uidl.getStringVariable("filter"))) {
  613. suggestionPopup.showSuggestions(currentSuggestions, currentPage,
  614. totalMatches);
  615. filtering = false;
  616. if (!popupOpenerClicked && lastIndex != -1) {
  617. // we're paging w/ arrows
  618. if (lastIndex == 0) {
  619. // going up, select last item
  620. int lastItem = PAGELENTH - 1;
  621. List items = suggestionPopup.menu.getItems();
  622. /*
  623. * The first page can contain less than 10 items if the null
  624. * selection item is filtered away
  625. */
  626. if (lastItem >= items.size()) {
  627. lastItem = items.size() - 1;
  628. }
  629. suggestionPopup.menu.selectItem((MenuItem) items
  630. .get(lastItem));
  631. } else {
  632. // going down, select first item
  633. suggestionPopup.menu
  634. .selectItem((MenuItem) suggestionPopup.menu
  635. .getItems().get(0));
  636. }
  637. lastIndex = -1; // reset
  638. }
  639. }
  640. // Calculate minumum textarea width
  641. suggestionPopupMinWidth = minWidth(captions);
  642. popupOpenerClicked = false;
  643. updateRootWidth();
  644. }
  645. private void setPromptingOn() {
  646. prompting = true;
  647. addStyleDependentName(CLASSNAME_PROMPT);
  648. tb.setText(inputPrompt);
  649. }
  650. private void setPromptingOff(String text) {
  651. tb.setText(text);
  652. prompting = false;
  653. removeStyleDependentName(CLASSNAME_PROMPT);
  654. }
  655. public void onSuggestionSelected(FilterSelectSuggestion suggestion) {
  656. currentSuggestion = suggestion;
  657. String newKey;
  658. if (suggestion.key.equals("")) {
  659. // "nullselection"
  660. newKey = "";
  661. } else {
  662. // normal selection
  663. newKey = String.valueOf(suggestion.getOptionKey());
  664. }
  665. String text = suggestion.getReplacementString();
  666. if ("".equals(newKey) && !focused) {
  667. setPromptingOn();
  668. } else {
  669. setPromptingOff(text);
  670. }
  671. setSelectedItemIcon(suggestion.getIconUri());
  672. if (!newKey.equals(selectedOptionKey)) {
  673. selectedOptionKey = newKey;
  674. client.updateVariable(paintableId, "selected",
  675. new String[] { selectedOptionKey }, immediate);
  676. // currentPage = -1; // forget the page
  677. }
  678. suggestionPopup.hide();
  679. }
  680. private void setSelectedItemIcon(String iconUri) {
  681. if (iconUri == null) {
  682. selectedItemIcon.setVisible(false);
  683. updateRootWidth();
  684. } else {
  685. selectedItemIcon.setUrl(iconUri);
  686. selectedItemIcon.setVisible(true);
  687. updateRootWidth();
  688. updateSelectedIconPosition();
  689. }
  690. }
  691. private void updateSelectedIconPosition() {
  692. // Position icon vertically to middle
  693. int availableHeight = getOffsetHeight();
  694. int iconHeight = Util.getRequiredHeight(selectedItemIcon);
  695. int marginTop = (availableHeight - iconHeight) / 2;
  696. DOM.setStyleAttribute(selectedItemIcon.getElement(), "marginTop",
  697. marginTop + "px");
  698. }
  699. public void onKeyDown(Widget sender, char keyCode, int modifiers) {
  700. if (enabled && suggestionPopup.isAttached()) {
  701. switch (keyCode) {
  702. case KeyboardListener.KEY_DOWN:
  703. suggestionPopup.selectNextItem();
  704. DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
  705. break;
  706. case KeyboardListener.KEY_UP:
  707. suggestionPopup.selectPrevItem();
  708. DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
  709. break;
  710. case KeyboardListener.KEY_PAGEDOWN:
  711. if (hasNextPage()) {
  712. filterOptions(currentPage + 1, lastFilter);
  713. }
  714. break;
  715. case KeyboardListener.KEY_PAGEUP:
  716. if (currentPage > 0) {
  717. filterOptions(currentPage - 1, lastFilter);
  718. }
  719. break;
  720. case KeyboardListener.KEY_ENTER:
  721. case KeyboardListener.KEY_TAB:
  722. suggestionPopup.menu.doSelectedItemAction();
  723. break;
  724. }
  725. }
  726. }
  727. public void onKeyPress(Widget sender, char keyCode, int modifiers) {
  728. }
  729. public void onKeyUp(Widget sender, char keyCode, int modifiers) {
  730. if (enabled) {
  731. switch (keyCode) {
  732. case KeyboardListener.KEY_ENTER:
  733. case KeyboardListener.KEY_TAB:
  734. case KeyboardListener.KEY_SHIFT:
  735. case KeyboardListener.KEY_CTRL:
  736. case KeyboardListener.KEY_ALT:
  737. ; // NOP
  738. break;
  739. case KeyboardListener.KEY_DOWN:
  740. case KeyboardListener.KEY_UP:
  741. case KeyboardListener.KEY_PAGEDOWN:
  742. case KeyboardListener.KEY_PAGEUP:
  743. if (suggestionPopup.isAttached()) {
  744. break;
  745. } else {
  746. // open popup as from gadget
  747. filterOptions(-1, "");
  748. lastFilter = "";
  749. tb.selectAll();
  750. break;
  751. }
  752. case KeyboardListener.KEY_ESCAPE:
  753. if (currentSuggestion != null) {
  754. String text = currentSuggestion.getReplacementString();
  755. setPromptingOff(text);
  756. selectedOptionKey = currentSuggestion.key;
  757. } else {
  758. setPromptingOn();
  759. selectedOptionKey = null;
  760. }
  761. lastFilter = "";
  762. suggestionPopup.hide();
  763. break;
  764. default:
  765. filterOptions(currentPage);
  766. break;
  767. }
  768. }
  769. }
  770. /**
  771. * Listener for popupopener
  772. */
  773. public void onClick(Widget sender) {
  774. if (enabled) {
  775. // ask suggestionPopup if it was just closed, we are using GWT
  776. // Popup's auto close feature
  777. if (!suggestionPopup.isJustClosed()) {
  778. filterOptions(-1, "");
  779. popupOpenerClicked = true;
  780. lastFilter = "";
  781. } else if (selectedOptionKey == null) {
  782. tb.setText(inputPrompt);
  783. prompting = true;
  784. }
  785. DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
  786. tb.setFocus(true);
  787. tb.selectAll();
  788. }
  789. }
  790. /*
  791. * Calculate minumum width for FilterSelect textarea
  792. */
  793. private native int minWidth(String captions)
  794. /*-{
  795. if(!captions || captions.length <= 0)
  796. return 0;
  797. captions = captions.split("|");
  798. var d = $wnd.document.createElement("div");
  799. var html = "";
  800. for(var i=0; i < captions.length; i++) {
  801. html += "<div>" + captions[i] + "</div>";
  802. // TODO apply same CSS classname as in suggestionmenu
  803. }
  804. d.style.position = "absolute";
  805. d.style.top = "0";
  806. d.style.left = "0";
  807. d.style.visibility = "hidden";
  808. d.innerHTML = html;
  809. $wnd.document.body.appendChild(d);
  810. var w = d.offsetWidth;
  811. $wnd.document.body.removeChild(d);
  812. return w;
  813. }-*/;
  814. public void onFocus(Widget sender) {
  815. focused = true;
  816. if (prompting) {
  817. setPromptingOff("");
  818. }
  819. addStyleDependentName("focus");
  820. }
  821. public void onLostFocus(Widget sender) {
  822. focused = false;
  823. if (!suggestionPopup.isAttached() || suggestionPopup.isJustClosed()) {
  824. // typing so fast the popup was never opened, or it's just closed
  825. suggestionPopup.menu.doSelectedItemAction();
  826. }
  827. if (selectedOptionKey == null) {
  828. setPromptingOn();
  829. }
  830. removeStyleDependentName("focus");
  831. }
  832. public void focus() {
  833. focused = true;
  834. if (prompting) {
  835. setPromptingOff("");
  836. }
  837. tb.setFocus(true);
  838. }
  839. @Override
  840. public void setWidth(String width) {
  841. if (width == null || width.equals("")) {
  842. this.width = null;
  843. } else {
  844. this.width = width;
  845. }
  846. Util.setWidthExcludingPaddingAndBorder(this, width, 4);
  847. updateRootWidth();
  848. }
  849. @Override
  850. public void setHeight(String height) {
  851. super.setHeight(height);
  852. Util.setHeightExcludingPaddingAndBorder(tb, height, 3);
  853. }
  854. private void updateRootWidth() {
  855. if (width == null) {
  856. /*
  857. * When the width is not specified we must specify width for root
  858. * div so the popupopener won't wrap to the next line and also so
  859. * the size of the combobox won't change over time.
  860. */
  861. int tbWidth = Util.getRequiredWidth(tb);
  862. int openerWidth = Util.getRequiredWidth(popupOpener);
  863. int iconWidth = Util.getRequiredWidth(selectedItemIcon);
  864. int w = tbWidth + openerWidth + iconWidth;
  865. if (suggestionPopupMinWidth > w) {
  866. setTextboxWidth(suggestionPopupMinWidth);
  867. w = suggestionPopupMinWidth;
  868. } else {
  869. /*
  870. * Firefox3 has its own way of doing rendering so we need to
  871. * specify the width for the TextField to make sure it actually
  872. * is rendered as wide as FF3 says it is
  873. */
  874. tb.setWidth((tbWidth - getTextboxPadding()) + "px");
  875. }
  876. super.setWidth((w) + "px");
  877. // Freeze the initial width, so that it won't change even if the
  878. // icon size changes
  879. width = w + "px";
  880. } else {
  881. /*
  882. * When the width is specified we also want to explicitly specify
  883. * widths for textbox and popupopener
  884. */
  885. setTextboxWidth(getMainWidth() - getComponentPadding());
  886. }
  887. }
  888. private int getMainWidth() {
  889. int componentWidth;
  890. if (BrowserInfo.get().isIE6()) {
  891. // Required in IE when textfield is wider than this.width
  892. DOM.setStyleAttribute(getElement(), "overflow", "hidden");
  893. componentWidth = getOffsetWidth();
  894. DOM.setStyleAttribute(getElement(), "overflow", "");
  895. } else {
  896. componentWidth = getOffsetWidth();
  897. }
  898. return componentWidth;
  899. }
  900. private void setTextboxWidth(int componentWidth) {
  901. int padding = getTextboxPadding();
  902. int popupOpenerWidth = Util.getRequiredWidth(popupOpener);
  903. int iconWidth = Util.getRequiredWidth(selectedItemIcon);
  904. int textboxWidth = componentWidth - padding - popupOpenerWidth
  905. - iconWidth;
  906. if (textboxWidth < 0) {
  907. textboxWidth = 0;
  908. }
  909. tb.setWidth(textboxWidth + "px");
  910. }
  911. private int getTextboxPadding() {
  912. if (textboxPadding < 0) {
  913. textboxPadding = Util.measureHorizontalPaddingAndBorder(tb
  914. .getElement(), 4);
  915. }
  916. return textboxPadding;
  917. }
  918. private int getComponentPadding() {
  919. if (componentPadding < 0) {
  920. componentPadding = Util.measureHorizontalPaddingAndBorder(
  921. getElement(), 3);
  922. }
  923. return componentPadding;
  924. }
  925. }