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.

ComboBoxConnector.java 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. /*
  2. * Copyright 2000-2018 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.ui.combobox;
  17. import java.util.List;
  18. import java.util.Objects;
  19. import java.util.logging.Logger;
  20. import com.vaadin.client.Profiler;
  21. import com.vaadin.client.annotations.OnStateChange;
  22. import com.vaadin.client.communication.StateChangeEvent;
  23. import com.vaadin.client.connectors.AbstractListingConnector;
  24. import com.vaadin.client.data.DataChangeHandler;
  25. import com.vaadin.client.data.DataSource;
  26. import com.vaadin.client.ui.SimpleManagedLayout;
  27. import com.vaadin.client.ui.VComboBox;
  28. import com.vaadin.client.ui.VComboBox.ComboBoxSuggestion;
  29. import com.vaadin.client.ui.VComboBox.DataReceivedHandler;
  30. import com.vaadin.shared.EventId;
  31. import com.vaadin.shared.Registration;
  32. import com.vaadin.shared.communication.FieldRpc.FocusAndBlurServerRpc;
  33. import com.vaadin.shared.data.DataCommunicatorConstants;
  34. import com.vaadin.shared.data.selection.SelectionServerRpc;
  35. import com.vaadin.shared.ui.Connect;
  36. import com.vaadin.shared.ui.combobox.ComboBoxClientRpc;
  37. import com.vaadin.shared.ui.combobox.ComboBoxConstants;
  38. import com.vaadin.shared.ui.combobox.ComboBoxServerRpc;
  39. import com.vaadin.shared.ui.combobox.ComboBoxState;
  40. import com.vaadin.ui.ComboBox;
  41. import elemental.json.JsonObject;
  42. @Connect(ComboBox.class)
  43. public class ComboBoxConnector extends AbstractListingConnector
  44. implements SimpleManagedLayout {
  45. private ComboBoxServerRpc rpc = getRpcProxy(ComboBoxServerRpc.class);
  46. private SelectionServerRpc selectionRpc = getRpcProxy(
  47. SelectionServerRpc.class);
  48. private FocusAndBlurServerRpc focusAndBlurRpc = getRpcProxy(
  49. FocusAndBlurServerRpc.class);
  50. private Registration dataChangeHandlerRegistration;
  51. /**
  52. * new item value that has been sent to server but selection handling hasn't
  53. * been performed for it yet
  54. */
  55. private String pendingNewItemValue = null;
  56. /**
  57. * If this flag is toggled, even unpaged data sources should be updated on
  58. * reset.
  59. */
  60. private boolean forceDataSourceUpdate = false;
  61. private boolean initialSelectionChangePending = true;
  62. @Override
  63. protected void init() {
  64. super.init();
  65. getWidget().connector = this;
  66. registerRpc(ComboBoxClientRpc.class, new ComboBoxClientRpc() {
  67. @Override
  68. public void newItemNotAdded(String itemValue) {
  69. if (itemValue != null && itemValue.equals(pendingNewItemValue)
  70. && isNewItemStillPending()) {
  71. // handled but not added, perform (de-)selection handling
  72. // immediately
  73. completeNewItemHandling();
  74. }
  75. }
  76. });
  77. }
  78. @Override
  79. public void onStateChanged(StateChangeEvent stateChangeEvent) {
  80. super.onStateChanged(stateChangeEvent);
  81. Profiler.enter("ComboBoxConnector.onStateChanged update content");
  82. VComboBox widget = getWidget();
  83. widget.readonly = isReadOnly();
  84. widget.updateReadOnly();
  85. // not a FocusWidget -> needs own tabindex handling
  86. widget.tb.setTabIndex(getState().tabIndex);
  87. widget.suggestionPopup.updateStyleNames(getState());
  88. // TODO if the pop up is opened, the actual item should be removed from
  89. // the popup (?)
  90. widget.nullSelectionAllowed = getState().emptySelectionAllowed;
  91. // TODO having this true would mean that the empty selection item comes
  92. // from the data source so none needs to be added - currently
  93. // unsupported
  94. widget.nullSelectItem = false;
  95. // make sure the input prompt is updated
  96. widget.updatePlaceholder();
  97. getDataReceivedHandler().serverReplyHandled();
  98. // all updates except options have been done
  99. widget.initDone = true;
  100. Profiler.leave("ComboBoxConnector.onStateChanged update content");
  101. }
  102. @OnStateChange("emptySelectionCaption")
  103. private void onEmptySelectionCaptionChange() {
  104. List<ComboBoxSuggestion> suggestions = getWidget().currentSuggestions;
  105. if (!suggestions.isEmpty() && isFirstPage()) {
  106. suggestions.remove(0);
  107. addEmptySelectionItem();
  108. }
  109. getWidget().setEmptySelectionCaption(getState().emptySelectionCaption);
  110. }
  111. @OnStateChange("forceDataSourceUpdate")
  112. private void onForceDataSourceUpdate() {
  113. forceDataSourceUpdate = getState().forceDataSourceUpdate;
  114. }
  115. @OnStateChange({ "selectedItemKey", "selectedItemCaption",
  116. "selectedItemIcon" })
  117. private void onSelectionChange() {
  118. if (getWidget().selectedOptionKey != getState().selectedItemKey) {
  119. if (initialSelectionChangePending) {
  120. getWidget().selectedOptionKey = getState().selectedItemKey;
  121. } else {
  122. getWidget().selectedOptionKey = null;
  123. getWidget().currentSuggestion = null;
  124. }
  125. initialSelectionChangePending = false;
  126. }
  127. clearNewItemHandlingIfMatch(getState().selectedItemCaption);
  128. getDataReceivedHandler().updateSelectionFromServer(
  129. getState().selectedItemKey, getState().selectedItemCaption,
  130. getState().selectedItemIcon);
  131. }
  132. @Override
  133. public VComboBox getWidget() {
  134. return (VComboBox) super.getWidget();
  135. }
  136. private DataReceivedHandler getDataReceivedHandler() {
  137. return getWidget().getDataReceivedHandler();
  138. }
  139. @Override
  140. public ComboBoxState getState() {
  141. return (ComboBoxState) super.getState();
  142. }
  143. @Override
  144. public void layout() {
  145. VComboBox widget = getWidget();
  146. if (widget.initDone) {
  147. widget.updateRootWidth();
  148. }
  149. }
  150. @Override
  151. public void setWidgetEnabled(boolean widgetEnabled) {
  152. super.setWidgetEnabled(widgetEnabled);
  153. getWidget().enabled = widgetEnabled;
  154. getWidget().tb.setEnabled(widgetEnabled);
  155. }
  156. /*
  157. * These methods exist to move communications out of VComboBox, and may be
  158. * refactored/removed in the future
  159. */
  160. /**
  161. * Send a message about a newly created item to the server.
  162. *
  163. * This method is for internal use only and may be removed in future
  164. * versions.
  165. *
  166. * @since 8.0
  167. * @param itemValue
  168. * user entered string value for the new item
  169. */
  170. public void sendNewItem(String itemValue) {
  171. if (itemValue != null && !itemValue.equals(pendingNewItemValue)) {
  172. // clear any previous handling as outdated
  173. clearNewItemHandling();
  174. pendingNewItemValue = itemValue;
  175. rpc.createNewItem(itemValue);
  176. getDataReceivedHandler().clearPendingNavigation();
  177. }
  178. }
  179. /**
  180. * Send a message to the server set the current filter.
  181. *
  182. * This method is for internal use only and may be removed in future
  183. * versions.
  184. *
  185. * @since 8.0
  186. * @param filter
  187. * the current filter string
  188. */
  189. protected void setFilter(String filter) {
  190. if (!Objects.equals(filter, getState().currentFilterText)) {
  191. getDataReceivedHandler().clearPendingNavigation();
  192. rpc.setFilter(filter);
  193. }
  194. }
  195. /**
  196. * Confirm with the widget that the pending new item value is still pending.
  197. *
  198. * This method is for internal use only and may be removed in future
  199. * versions.
  200. *
  201. * @return {@code true} if the value is still pending, {@code false} if
  202. * there is no pending value or it doesn't match
  203. */
  204. private boolean isNewItemStillPending() {
  205. return getDataReceivedHandler().isPending(pendingNewItemValue);
  206. }
  207. /**
  208. * Send a message to the server to request a page of items with the current
  209. * filter.
  210. *
  211. * This method is for internal use only and may be removed in future
  212. * versions.
  213. *
  214. * @since 8.0
  215. * @param page
  216. * the page number to get or -1 to let the server/connector
  217. * decide based on current selection (possibly loading more data
  218. * from the server)
  219. * @param filter
  220. * the filter to apply, never {@code null}
  221. */
  222. public void requestPage(int page, String filter) {
  223. setFilter(filter);
  224. if (page < 0) {
  225. if (getState().scrollToSelectedItem) {
  226. // TODO this should be optimized not to try to fetch everything
  227. getDataSource().ensureAvailability(0, getDataSource().size());
  228. return;
  229. }
  230. page = 0;
  231. }
  232. VComboBox widget = getWidget();
  233. int adjustment = widget.nullSelectionAllowed && filter.isEmpty() ? 1
  234. : 0;
  235. int startIndex = Math.max(0, page * widget.pageLength - adjustment);
  236. int pageLength = widget.pageLength > 0 ? widget.pageLength
  237. : getDataSource().size();
  238. getDataSource().ensureAvailability(startIndex, pageLength);
  239. }
  240. /**
  241. * Send a message to the server updating the current selection.
  242. *
  243. * This method is for internal use only and may be removed in future
  244. * versions.
  245. *
  246. * @since 8.0
  247. * @param selectionKey
  248. * the current selected item key
  249. */
  250. public void sendSelection(String selectionKey) {
  251. // map also the special empty string option key (from data change
  252. // handler below) to null
  253. selectionRpc.select("".equals(selectionKey) ? null : selectionKey);
  254. getDataReceivedHandler().clearPendingNavigation();
  255. }
  256. /**
  257. * Notify the server that the combo box received focus.
  258. *
  259. * For timing reasons, ConnectorFocusAndBlurHandler is not used at the
  260. * moment.
  261. *
  262. * This method is for internal use only and may be removed in future
  263. * versions.
  264. *
  265. * @since 8.0
  266. */
  267. public void sendFocusEvent() {
  268. boolean registeredListeners = hasEventListener(EventId.FOCUS);
  269. if (registeredListeners) {
  270. focusAndBlurRpc.focus();
  271. getDataReceivedHandler().clearPendingNavigation();
  272. }
  273. }
  274. /**
  275. * Notify the server that the combo box lost focus.
  276. *
  277. * For timing reasons, ConnectorFocusAndBlurHandler is not used at the
  278. * moment.
  279. *
  280. * This method is for internal use only and may be removed in future
  281. * versions.
  282. *
  283. * @since 8.0
  284. */
  285. public void sendBlurEvent() {
  286. boolean registeredListeners = hasEventListener(EventId.BLUR);
  287. if (registeredListeners) {
  288. focusAndBlurRpc.blur();
  289. getDataReceivedHandler().clearPendingNavigation();
  290. }
  291. }
  292. @Override
  293. public void setDataSource(DataSource<JsonObject> dataSource) {
  294. super.setDataSource(dataSource);
  295. dataChangeHandlerRegistration = dataSource
  296. .addDataChangeHandler(new PagedDataChangeHandler(dataSource));
  297. }
  298. @Override
  299. public void onUnregister() {
  300. super.onUnregister();
  301. dataChangeHandlerRegistration.remove();
  302. }
  303. @Override
  304. public boolean isRequiredIndicatorVisible() {
  305. return getState().required && !isReadOnly();
  306. }
  307. private void refreshData() {
  308. updateCurrentPage();
  309. int start = getWidget().currentPage * getWidget().pageLength;
  310. int end = getWidget().pageLength > 0 ? start + getWidget().pageLength
  311. : getDataSource().size();
  312. getWidget().currentSuggestions.clear();
  313. if (getWidget().getNullSelectionItemShouldBeVisible()) {
  314. // add special null selection item...
  315. if (isFirstPage()) {
  316. addEmptySelectionItem();
  317. } else {
  318. // ...or leave space for it
  319. start = start - 1;
  320. }
  321. // in either case, the last item to show is
  322. // shifted by one, unless no paging is used
  323. if (getState().pageLength != 0) {
  324. end = end - 1;
  325. }
  326. }
  327. updateSuggestions(start, end);
  328. getWidget().setTotalSuggestions(getDataSource().size());
  329. getWidget().resetLastNewItemString();
  330. getDataReceivedHandler().dataReceived();
  331. }
  332. private void updateSuggestions(int start, int end) {
  333. for (int i = start; i < end; ++i) {
  334. JsonObject row = getDataSource().getRow(i);
  335. if (row != null) {
  336. String key = getRowKey(row);
  337. String caption = row.getString(DataCommunicatorConstants.NAME);
  338. String style = row.getString(ComboBoxConstants.STYLE);
  339. String untranslatedIconUri = row
  340. .getString(ComboBoxConstants.ICON);
  341. ComboBoxSuggestion suggestion = getWidget().new ComboBoxSuggestion(
  342. key, caption, style, untranslatedIconUri);
  343. getWidget().currentSuggestions.add(suggestion);
  344. } else {
  345. // there is not enough options to fill the page
  346. return;
  347. }
  348. }
  349. }
  350. private boolean isFirstPage() {
  351. return getWidget().currentPage == 0;
  352. }
  353. private void addEmptySelectionItem() {
  354. if (isFirstPage()) {
  355. getWidget().currentSuggestions.add(0,
  356. getWidget().new ComboBoxSuggestion("",
  357. getState().emptySelectionCaption, null, null));
  358. }
  359. }
  360. private void updateCurrentPage() {
  361. // try to find selected item if requested
  362. if (getState().scrollToSelectedItem && getState().pageLength > 0
  363. && getWidget().currentPage < 0
  364. && getWidget().selectedOptionKey != null) {
  365. // search for the item with the selected key
  366. getWidget().currentPage = 0;
  367. for (int i = 0; i < getDataSource().size(); ++i) {
  368. JsonObject row = getDataSource().getRow(i);
  369. if (row != null) {
  370. String key = getRowKey(row);
  371. if (getWidget().selectedOptionKey.equals(key)) {
  372. if (getWidget().nullSelectionAllowed) {
  373. getWidget().currentPage = (i + 1)
  374. / getState().pageLength;
  375. } else {
  376. getWidget().currentPage = i / getState().pageLength;
  377. }
  378. break;
  379. }
  380. }
  381. }
  382. } else if (getWidget().currentPage < 0) {
  383. getWidget().currentPage = 0;
  384. }
  385. }
  386. /**
  387. * If previous calls to refreshData haven't sorted out the selection yet,
  388. * enforce it.
  389. *
  390. * This method is for internal use only and may be removed in future
  391. * versions.
  392. */
  393. private void completeNewItemHandling() {
  394. // ensure the widget hasn't got a new selection in the meantime
  395. if (isNewItemStillPending()) {
  396. // mark new item for selection handling on the widget
  397. getWidget().suggestionPopup.menu
  398. .markNewItemsHandled(pendingNewItemValue);
  399. // clear pending value
  400. pendingNewItemValue = null;
  401. // trigger the final selection handling
  402. refreshData();
  403. } else {
  404. clearNewItemHandling();
  405. }
  406. }
  407. /**
  408. * Clears the pending new item value if the widget's pending value no longer
  409. * matches.
  410. *
  411. * This method is for internal use only and may be removed in future
  412. * versions.
  413. */
  414. private void clearNewItemHandling() {
  415. pendingNewItemValue = null;
  416. }
  417. /**
  418. * Clears the new item handling variables if the given value matches the
  419. * pending value.
  420. *
  421. * This method is for internal use only and may be removed in future
  422. * versions.
  423. *
  424. * @param value
  425. * already handled value
  426. */
  427. public void clearNewItemHandlingIfMatch(String value) {
  428. if (value != null && value.equals(pendingNewItemValue)) {
  429. pendingNewItemValue = null;
  430. }
  431. }
  432. private static final Logger LOGGER = Logger
  433. .getLogger(ComboBoxConnector.class.getName());
  434. private class PagedDataChangeHandler implements DataChangeHandler {
  435. private final DataSource<?> dataSource;
  436. public PagedDataChangeHandler(DataSource<?> dataSource) {
  437. this.dataSource = dataSource;
  438. }
  439. @Override
  440. public void dataUpdated(int firstRowIndex, int numberOfRows) {
  441. // NOOP since dataAvailable is always triggered afterwards
  442. }
  443. @Override
  444. public void dataRemoved(int firstRowIndex, int numberOfRows) {
  445. // NOOP since dataAvailable is always triggered afterwards
  446. }
  447. @Override
  448. public void dataAdded(int firstRowIndex, int numberOfRows) {
  449. // NOOP since dataAvailable is always triggered afterwards
  450. }
  451. @Override
  452. public void dataAvailable(int firstRowIndex, int numberOfRows) {
  453. refreshData();
  454. }
  455. @Override
  456. public void resetDataAndSize(int estimatedNewDataSize) {
  457. if (getState().pageLength == 0) {
  458. if (getWidget().suggestionPopup.isShowing()
  459. || forceDataSourceUpdate) {
  460. dataSource.ensureAvailability(0, estimatedNewDataSize);
  461. }
  462. if (forceDataSourceUpdate) {
  463. rpc.resetForceDataSourceUpdate();
  464. }
  465. // else lets just wait till the popup is opened before
  466. // everything is fetched to it. this could be optimized later on
  467. // to fetch everything if in-memory data is used.
  468. } else {
  469. // reset data: clear any current options, set page to 0
  470. getWidget().currentPage = 0;
  471. getWidget().currentSuggestions.clear();
  472. dataSource.ensureAvailability(0, getState().pageLength);
  473. }
  474. }
  475. }
  476. }