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.

DefaultEditorEventHandler.java 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  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.widget.grid;
  17. import com.google.gwt.core.client.Duration;
  18. import com.google.gwt.dom.client.BrowserEvents;
  19. import com.google.gwt.dom.client.Element;
  20. import com.google.gwt.event.dom.client.KeyCodes;
  21. import com.google.gwt.user.client.Event;
  22. import com.google.gwt.user.client.ui.Widget;
  23. import com.vaadin.client.WidgetUtil;
  24. import com.vaadin.client.ui.FocusUtil;
  25. import com.vaadin.client.widgets.Grid;
  26. import com.vaadin.client.widgets.Grid.Editor;
  27. import com.vaadin.client.widgets.Grid.EditorDomEvent;
  28. import java.util.List;
  29. /**
  30. * The default handler for Grid editor events. Offers several overridable
  31. * protected methods for easier customization.
  32. *
  33. * @since 7.6
  34. * @author Vaadin Ltd
  35. */
  36. public class DefaultEditorEventHandler<T> implements Editor.EventHandler<T> {
  37. public static final int KEYCODE_OPEN = KeyCodes.KEY_ENTER;
  38. public static final int KEYCODE_MOVE_VERTICAL = KeyCodes.KEY_ENTER;
  39. public static final int KEYCODE_CLOSE = KeyCodes.KEY_ESCAPE;
  40. public static final int KEYCODE_MOVE_HORIZONTAL = KeyCodes.KEY_TAB;
  41. public static final int KEYCODE_BUFFERED_SAVE = KeyCodes.KEY_ENTER;
  42. private double lastTouchEventTime = 0;
  43. private int lastTouchEventX = -1;
  44. private int lastTouchEventY = -1;
  45. private int lastTouchEventRow = -1;
  46. /**
  47. * Returns whether the given event is a touch event that should open the
  48. * editor.
  49. *
  50. * @param event
  51. * the received event
  52. * @return whether the event is a touch open event
  53. */
  54. protected boolean isTouchOpenEvent(EditorDomEvent<T> event) {
  55. final Event e = event.getDomEvent();
  56. final int type = e.getTypeInt();
  57. final double now = Duration.currentTimeMillis();
  58. final int currentX = WidgetUtil.getTouchOrMouseClientX(e);
  59. final int currentY = WidgetUtil.getTouchOrMouseClientY(e);
  60. final boolean validTouchOpenEvent = type == Event.ONTOUCHEND
  61. && now - lastTouchEventTime < 500
  62. && lastTouchEventRow == event.getCell().getRowIndex()
  63. && Math.abs(lastTouchEventX - currentX) < 20
  64. && Math.abs(lastTouchEventY - currentY) < 20;
  65. if (type == Event.ONTOUCHSTART) {
  66. lastTouchEventX = currentX;
  67. lastTouchEventY = currentY;
  68. }
  69. if (type == Event.ONTOUCHEND) {
  70. lastTouchEventTime = now;
  71. lastTouchEventRow = event.getCell().getRowIndex();
  72. }
  73. return validTouchOpenEvent;
  74. }
  75. /**
  76. * Returns whether the given event should open the editor. The default
  77. * implementation returns true if and only if the event is a doubleclick or
  78. * if it is a keydown event and the keycode is {@link #KEYCODE_OPEN}.
  79. *
  80. * @param event
  81. * the received event
  82. * @return true if the event is an open event, false otherwise
  83. */
  84. protected boolean isOpenEvent(EditorDomEvent<T> event) {
  85. final Event e = event.getDomEvent();
  86. return e.getTypeInt() == Event.ONDBLCLICK
  87. || (e.getTypeInt() == Event.ONKEYDOWN
  88. && e.getKeyCode() == KEYCODE_OPEN)
  89. || isTouchOpenEvent(event);
  90. }
  91. /**
  92. * Opens the editor on the appropriate row if the received event is an open
  93. * event. The default implementation uses
  94. * {@link #isOpenEvent(EditorDomEvent) isOpenEvent}.
  95. *
  96. * @param event
  97. * the received event
  98. * @return true if this method handled the event and nothing else should be
  99. * done, false otherwise
  100. */
  101. protected boolean handleOpenEvent(EditorDomEvent<T> event) {
  102. if (isOpenEvent(event)) {
  103. final EventCellReference<T> cell = event.getCell();
  104. editRow(event, cell.getRowIndex(), cell.getColumnIndexDOM());
  105. event.getDomEvent().preventDefault();
  106. return true;
  107. }
  108. return false;
  109. }
  110. /**
  111. * Moves the editor to another row or another column if the received event
  112. * is a move event. The default implementation moves the editor to the
  113. * clicked row if the event is a click; otherwise, if the event is a keydown
  114. * and the keycode is {@link #KEYCODE_MOVE_VERTICAL}, moves the editor one
  115. * row up or down if the shift key is pressed or not, respectively. Keydown
  116. * event with keycode {@link #KEYCODE_MOVE_HORIZONTAL} moves the editor left
  117. * or right if shift key is pressed or not, respectively.
  118. *
  119. * @param event
  120. * the received event
  121. * @return true if this method handled the event and nothing else should be
  122. * done, false otherwise
  123. */
  124. protected boolean handleMoveEvent(EditorDomEvent<T> event) {
  125. Event e = event.getDomEvent();
  126. final EventCellReference<T> cell = event.getCell();
  127. // TODO: Move on touch events
  128. if (e.getTypeInt() == Event.ONCLICK) {
  129. editRow(event, cell.getRowIndex(), cell.getColumnIndexDOM());
  130. return true;
  131. } else if (e.getTypeInt() == Event.ONKEYDOWN) {
  132. int rowDelta = 0;
  133. int colDelta = 0;
  134. if (e.getKeyCode() == KEYCODE_MOVE_VERTICAL) {
  135. rowDelta = (e.getShiftKey() ? -1 : +1);
  136. } else if (e.getKeyCode() == KEYCODE_MOVE_HORIZONTAL) {
  137. colDelta = (e.getShiftKey() ? -1 : +1);
  138. // Prevent tab out of Grid Editor
  139. event.getDomEvent().preventDefault();
  140. }
  141. final boolean changed = rowDelta != 0 || colDelta != 0;
  142. if (changed) {
  143. int columnCount = event.getGrid().getVisibleColumns().size();
  144. int colIndex = colDelta > 0
  145. ? findNextEditableColumnIndex(event.getGrid(),
  146. event.getFocusedColumnIndex() + colDelta)
  147. : findPrevEditableColumnIndex(event.getGrid(),
  148. event.getFocusedColumnIndex() + colDelta);
  149. int rowIndex = event.getRowIndex();
  150. // Handle row change with horizontal move when column goes out
  151. // of range.
  152. if (rowDelta == 0 && colIndex < 0) {
  153. if (colDelta > 0
  154. && rowIndex < event.getGrid().getDataSource().size()
  155. - 1) {
  156. rowDelta = 1;
  157. colIndex = findNextEditableColumnIndex(event.getGrid(),
  158. 0);
  159. } else if (colDelta < 0 && rowIndex > 0) {
  160. rowDelta = -1;
  161. colIndex = findPrevEditableColumnIndex(event.getGrid(),
  162. columnCount - 1);
  163. }
  164. }
  165. editRow(event, rowIndex + rowDelta, colIndex);
  166. }
  167. return changed;
  168. }
  169. return false;
  170. }
  171. /**
  172. * Finds index of the first editable column, starting at the specified
  173. * index.
  174. *
  175. * @param grid
  176. * the current grid, not null.
  177. * @param startingWith
  178. * start with this column. Index into the
  179. * {@link Grid#getVisibleColumns()}.
  180. * @return the index of the nearest visible column; may return the
  181. * <code>startingWith</code> itself. Returns -1 if there is no such
  182. * column.
  183. */
  184. private int findNextEditableColumnIndex(Grid<T> grid, int startingWith) {
  185. final List<Grid.Column<?, T>> columns = grid.getVisibleColumns();
  186. for (int i = startingWith; i < columns.size(); i++) {
  187. if (columns.get(i).isEditable()) {
  188. return i;
  189. }
  190. }
  191. return -1;
  192. }
  193. /**
  194. * Finds index of the last editable column, searching backwards starting at
  195. * the specified index.
  196. *
  197. * @param grid
  198. * the current grid, not null.
  199. * @param startingWith
  200. * start with this column. Index into the
  201. * {@link Grid#getVisibleColumns()}.
  202. * @return the index of the nearest visible column; may return the
  203. * <code>startingWith</code> itself. Returns -1 if there is no such
  204. * column.
  205. */
  206. private int findPrevEditableColumnIndex(Grid<T> grid, int startingWith) {
  207. final List<Grid.Column<?, T>> columns = grid.getVisibleColumns();
  208. for (int i = startingWith; i >= 0; i--) {
  209. if (columns.get(i).isEditable()) {
  210. return i;
  211. }
  212. }
  213. return -1;
  214. }
  215. /**
  216. * Moves the editor to another column if the received event is a move event.
  217. * By default the editor is moved on a keydown event with keycode
  218. * {@link #KEYCODE_MOVE_HORIZONTAL}. This moves the editor left or right if
  219. * shift key is pressed or not, respectively.
  220. *
  221. * @param event
  222. * the received event
  223. * @return true if this method handled the event and nothing else should be
  224. * done, false otherwise
  225. */
  226. protected boolean handleBufferedMoveEvent(EditorDomEvent<T> event) {
  227. Event e = event.getDomEvent();
  228. if (e.getType().equals(BrowserEvents.CLICK)
  229. && event.getRowIndex() == event.getCell().getRowIndex()) {
  230. editRow(event, event.getRowIndex(),
  231. event.getCell().getColumnIndexDOM());
  232. return true;
  233. } else if (e.getType().equals(BrowserEvents.KEYDOWN)
  234. && e.getKeyCode() == KEYCODE_MOVE_HORIZONTAL) {
  235. // Prevent tab out of Grid Editor
  236. event.getDomEvent().preventDefault();
  237. final int newColIndex = e.getShiftKey()
  238. ? findPrevEditableColumnIndex(event.getGrid(),
  239. event.getFocusedColumnIndex() - 1)
  240. : findNextEditableColumnIndex(event.getGrid(),
  241. event.getFocusedColumnIndex() + 1);
  242. if (newColIndex >= 0) {
  243. editRow(event, event.getRowIndex(), newColIndex);
  244. }
  245. return true;
  246. } else if (e.getType().equals(BrowserEvents.KEYDOWN)
  247. && e.getKeyCode() == KEYCODE_BUFFERED_SAVE) {
  248. triggerValueChangeEvent(event);
  249. // Save and close.
  250. event.getGrid().getEditor().save();
  251. FocusUtil.setFocus(event.getGrid(), true);
  252. return true;
  253. }
  254. return false;
  255. }
  256. /**
  257. * Returns whether the given event should close the editor. The default
  258. * implementation returns true if and only if the event is a keydown event
  259. * and the keycode is {@link #KEYCODE_CLOSE}.
  260. *
  261. * @param event
  262. * the received event
  263. * @return true if the event is a close event, false otherwise
  264. */
  265. protected boolean isCloseEvent(EditorDomEvent<T> event) {
  266. final Event e = event.getDomEvent();
  267. return e.getTypeInt() == Event.ONKEYDOWN
  268. && e.getKeyCode() == KEYCODE_CLOSE;
  269. }
  270. /**
  271. * Closes the editor if the received event is a close event. The default
  272. * implementation uses {@link #isCloseEvent(EditorDomEvent) isCloseEvent}.
  273. *
  274. * @param event
  275. * the received event
  276. * @return true if this method handled the event and nothing else should be
  277. * done, false otherwise
  278. */
  279. protected boolean handleCloseEvent(EditorDomEvent<T> event) {
  280. if (isCloseEvent(event)) {
  281. event.getEditor().cancel();
  282. FocusUtil.setFocus(event.getGrid(), true);
  283. return true;
  284. }
  285. return false;
  286. }
  287. protected void editRow(EditorDomEvent<T> event, int rowIndex,
  288. int colIndex) {
  289. int rowCount = event.getGrid().getDataSource().size();
  290. // Limit rowIndex between 0 and rowCount - 1
  291. rowIndex = Math.max(0, Math.min(rowCount - 1, rowIndex));
  292. int colCount = event.getGrid().getVisibleColumns().size();
  293. // Limit colIndex between 0 and colCount - 1
  294. colIndex = Math.max(0, Math.min(colCount - 1, colIndex));
  295. if (rowIndex != event.getRowIndex()) {
  296. triggerValueChangeEvent(event);
  297. }
  298. event.getEditor().editRow(rowIndex, colIndex);
  299. }
  300. /**
  301. * Triggers a value change event from the editor field if it has focus. This
  302. * is based on the assumption that editor field will fire the value change
  303. * when a blur event occurs.
  304. *
  305. * @param event
  306. * the editor DOM event
  307. */
  308. private void triggerValueChangeEvent(EditorDomEvent<T> event) {
  309. // Force a blur to cause a value change event
  310. Widget editorWidget = event.getEditorWidget();
  311. if (editorWidget != null) {
  312. Element focusedElement = WidgetUtil.getFocusedElement();
  313. if (editorWidget.getElement().isOrHasChild(focusedElement)) {
  314. focusedElement.blur();
  315. focusedElement.focus();
  316. }
  317. }
  318. }
  319. @Override
  320. public boolean handleEvent(EditorDomEvent<T> event) {
  321. final Editor<T> editor = event.getEditor();
  322. final boolean isBody = event.getCell().isBody();
  323. final boolean handled;
  324. if (event.getGrid().isEditorActive()) {
  325. handled = handleCloseEvent(event)
  326. || (!editor.isBuffered() && isBody
  327. && handleMoveEvent(event))
  328. || (editor.isBuffered() && isBody
  329. && handleBufferedMoveEvent(event));
  330. } else {
  331. handled = event.getGrid().isEnabled() && isBody
  332. && handleOpenEvent(event);
  333. }
  334. // Buffered mode should swallow all events, if not already handled.
  335. boolean swallowEvent = event.getGrid().isEditorActive()
  336. && editor.isBuffered();
  337. return handled || swallowEvent;
  338. }
  339. }