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

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