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.

VTextField.java 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. /*
  2. @VaadinApache2LicenseForJavaFiles@
  3. */
  4. package com.vaadin.terminal.gwt.client.ui;
  5. import com.google.gwt.core.client.Scheduler;
  6. import com.google.gwt.dom.client.Style.Overflow;
  7. import com.google.gwt.event.dom.client.BlurEvent;
  8. import com.google.gwt.event.dom.client.BlurHandler;
  9. import com.google.gwt.event.dom.client.ChangeEvent;
  10. import com.google.gwt.event.dom.client.ChangeHandler;
  11. import com.google.gwt.event.dom.client.FocusEvent;
  12. import com.google.gwt.event.dom.client.FocusHandler;
  13. import com.google.gwt.event.dom.client.KeyCodes;
  14. import com.google.gwt.event.dom.client.KeyDownEvent;
  15. import com.google.gwt.event.dom.client.KeyDownHandler;
  16. import com.google.gwt.user.client.Command;
  17. import com.google.gwt.user.client.DOM;
  18. import com.google.gwt.user.client.Element;
  19. import com.google.gwt.user.client.Event;
  20. import com.google.gwt.user.client.Timer;
  21. import com.google.gwt.user.client.ui.TextBoxBase;
  22. import com.google.gwt.user.client.ui.Widget;
  23. import com.vaadin.terminal.gwt.client.ApplicationConnection;
  24. import com.vaadin.terminal.gwt.client.BrowserInfo;
  25. import com.vaadin.terminal.gwt.client.EventId;
  26. import com.vaadin.terminal.gwt.client.VPaintableWidget;
  27. import com.vaadin.terminal.gwt.client.UIDL;
  28. import com.vaadin.terminal.gwt.client.Util;
  29. import com.vaadin.terminal.gwt.client.VTooltip;
  30. import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.BeforeShortcutActionListener;
  31. /**
  32. * This class represents a basic text input field with one row.
  33. *
  34. * @author Vaadin Ltd.
  35. *
  36. */
  37. public class VTextField extends TextBoxBase implements VPaintableWidget, Field,
  38. ChangeHandler, FocusHandler, BlurHandler, BeforeShortcutActionListener,
  39. KeyDownHandler {
  40. public static final String VAR_CUR_TEXT = "curText";
  41. public static final String ATTR_NO_VALUE_CHANGE_BETWEEN_PAINTS = "nvc";
  42. /**
  43. * The input node CSS classname.
  44. */
  45. public static final String CLASSNAME = "v-textfield";
  46. /**
  47. * This CSS classname is added to the input node on hover.
  48. */
  49. public static final String CLASSNAME_FOCUS = "focus";
  50. protected String id;
  51. protected ApplicationConnection client;
  52. private String valueBeforeEdit = null;
  53. /**
  54. * Set to false if a text change event has been sent since the last value
  55. * change event. This means that {@link #valueBeforeEdit} should not be
  56. * trusted when determining whether a text change even should be sent.
  57. */
  58. private boolean valueBeforeEditIsSynced = true;
  59. private boolean immediate = false;
  60. private int extraHorizontalPixels = -1;
  61. private int extraVerticalPixels = -1;
  62. private int maxLength = -1;
  63. private static final String CLASSNAME_PROMPT = "prompt";
  64. private static final String ATTR_INPUTPROMPT = "prompt";
  65. public static final String ATTR_TEXTCHANGE_TIMEOUT = "iet";
  66. public static final String VAR_CURSOR = "c";
  67. public static final String ATTR_TEXTCHANGE_EVENTMODE = "iem";
  68. private static final String TEXTCHANGE_MODE_EAGER = "EAGER";
  69. private static final String TEXTCHANGE_MODE_TIMEOUT = "TIMEOUT";
  70. private String inputPrompt = null;
  71. private boolean prompting = false;
  72. private int lastCursorPos = -1;
  73. private boolean wordwrap = true;
  74. public VTextField() {
  75. this(DOM.createInputText());
  76. }
  77. protected VTextField(Element node) {
  78. super(node);
  79. if (BrowserInfo.get().getIEVersion() > 0
  80. && BrowserInfo.get().getIEVersion() < 8) {
  81. // Fixes IE margin problem (#2058)
  82. DOM.setStyleAttribute(node, "marginTop", "-1px");
  83. DOM.setStyleAttribute(node, "marginBottom", "-1px");
  84. }
  85. setStyleName(CLASSNAME);
  86. addChangeHandler(this);
  87. if (BrowserInfo.get().isIE()) {
  88. // IE does not send change events when pressing enter in a text
  89. // input so we handle it using a key listener instead
  90. addKeyDownHandler(this);
  91. }
  92. addFocusHandler(this);
  93. addBlurHandler(this);
  94. sinkEvents(VTooltip.TOOLTIP_EVENTS);
  95. }
  96. /*
  97. * TODO When GWT adds ONCUT, add it there and remove workaround. See
  98. * http://code.google.com/p/google-web-toolkit/issues/detail?id=4030
  99. *
  100. * Also note that the cut/paste are not totally crossbrowsers compatible.
  101. * E.g. in Opera mac works via context menu, but on via File->Paste/Cut.
  102. * Opera might need the polling method for 100% working textchanceevents.
  103. * Eager polling for a change is bit dum and heavy operation, so I guess we
  104. * should first try to survive without.
  105. */
  106. private static final int TEXTCHANGE_EVENTS = Event.ONPASTE
  107. | Event.KEYEVENTS | Event.ONMOUSEUP;
  108. @Override
  109. public void onBrowserEvent(Event event) {
  110. super.onBrowserEvent(event);
  111. if (client != null) {
  112. client.handleTooltipEvent(event, this);
  113. }
  114. if (listenTextChangeEvents
  115. && (event.getTypeInt() & TEXTCHANGE_EVENTS) == event
  116. .getTypeInt()) {
  117. deferTextChangeEvent();
  118. }
  119. }
  120. /*
  121. * TODO optimize this so that only changes are sent + make the value change
  122. * event just a flag that moves the current text to value
  123. */
  124. private String lastTextChangeString = null;
  125. private String getLastCommunicatedString() {
  126. return lastTextChangeString;
  127. }
  128. private void communicateTextValueToServer() {
  129. String text = getText();
  130. if (prompting) {
  131. // Input prompt visible, text is actually ""
  132. text = "";
  133. }
  134. if (!text.equals(getLastCommunicatedString())) {
  135. if (valueBeforeEditIsSynced && text.equals(valueBeforeEdit)) {
  136. /*
  137. * Value change for the current text has been enqueued since the
  138. * last text change event was sent, but we can't know that it
  139. * has been sent to the server. Ensure that all pending changes
  140. * are sent now. Sending a value change without a text change
  141. * will simulate a TextChangeEvent on the server.
  142. */
  143. client.sendPendingVariableChanges();
  144. } else {
  145. // Default case - just send an immediate text change message
  146. client.updateVariable(id, VAR_CUR_TEXT, text, true);
  147. // Shouldn't investigate valueBeforeEdit to avoid duplicate text
  148. // change events as the states are not in sync any more
  149. valueBeforeEditIsSynced = false;
  150. }
  151. lastTextChangeString = text;
  152. }
  153. }
  154. private Timer textChangeEventTrigger = new Timer() {
  155. @Override
  156. public void run() {
  157. if (isAttached()) {
  158. updateCursorPosition();
  159. communicateTextValueToServer();
  160. scheduled = false;
  161. }
  162. }
  163. };
  164. private boolean scheduled = false;
  165. private boolean listenTextChangeEvents;
  166. private String textChangeEventMode;
  167. private int textChangeEventTimeout;
  168. private void deferTextChangeEvent() {
  169. if (textChangeEventMode.equals(TEXTCHANGE_MODE_TIMEOUT) && scheduled) {
  170. return;
  171. } else {
  172. textChangeEventTrigger.cancel();
  173. }
  174. textChangeEventTrigger.schedule(getTextChangeEventTimeout());
  175. scheduled = true;
  176. }
  177. private int getTextChangeEventTimeout() {
  178. return textChangeEventTimeout;
  179. }
  180. @Override
  181. public void setReadOnly(boolean readOnly) {
  182. boolean wasReadOnly = isReadOnly();
  183. if (readOnly) {
  184. setTabIndex(-1);
  185. } else if (wasReadOnly && !readOnly && getTabIndex() == -1) {
  186. /*
  187. * Need to manually set tab index to 0 since server will not send
  188. * the tab index if it is 0.
  189. */
  190. setTabIndex(0);
  191. }
  192. super.setReadOnly(readOnly);
  193. }
  194. public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
  195. this.client = client;
  196. id = uidl.getId();
  197. if (client.updateComponent(this, uidl, true)) {
  198. return;
  199. }
  200. if (uidl.getBooleanAttribute("readonly")) {
  201. setReadOnly(true);
  202. } else {
  203. setReadOnly(false);
  204. }
  205. inputPrompt = uidl.getStringAttribute(ATTR_INPUTPROMPT);
  206. setMaxLength(uidl.hasAttribute("maxLength") ? uidl
  207. .getIntAttribute("maxLength") : -1);
  208. immediate = uidl.getBooleanAttribute("immediate");
  209. listenTextChangeEvents = client.hasEventListeners(this, "ie");
  210. if (listenTextChangeEvents) {
  211. textChangeEventMode = uidl
  212. .getStringAttribute(ATTR_TEXTCHANGE_EVENTMODE);
  213. if (textChangeEventMode.equals(TEXTCHANGE_MODE_EAGER)) {
  214. textChangeEventTimeout = 1;
  215. } else {
  216. textChangeEventTimeout = uidl
  217. .getIntAttribute(ATTR_TEXTCHANGE_TIMEOUT);
  218. if (textChangeEventTimeout < 1) {
  219. // Sanitize and allow lazy/timeout with timeout set to 0 to
  220. // work as eager
  221. textChangeEventTimeout = 1;
  222. }
  223. }
  224. sinkEvents(TEXTCHANGE_EVENTS);
  225. attachCutEventListener(getElement());
  226. }
  227. if (uidl.hasAttribute("cols")) {
  228. setColumns(new Integer(uidl.getStringAttribute("cols")).intValue());
  229. }
  230. final String text = uidl.getStringVariable("text");
  231. /*
  232. * We skip the text content update if field has been repainted, but text
  233. * has not been changed. Additional sanity check verifies there is no
  234. * change in the que (in which case we count more on the server side
  235. * value).
  236. */
  237. if (!(uidl.getBooleanAttribute(ATTR_NO_VALUE_CHANGE_BETWEEN_PAINTS)
  238. && valueBeforeEdit != null && text.equals(valueBeforeEdit))) {
  239. updateFieldContent(text);
  240. }
  241. if (uidl.hasAttribute("selpos")) {
  242. final int pos = uidl.getIntAttribute("selpos");
  243. final int length = uidl.getIntAttribute("sellen");
  244. /*
  245. * Gecko defers setting the text so we need to defer the selection.
  246. */
  247. Scheduler.get().scheduleDeferred(new Command() {
  248. public void execute() {
  249. setSelectionRange(pos, length);
  250. }
  251. });
  252. }
  253. // Here for backward compatibility; to be moved to TextArea.
  254. // Optimization: server does not send attribute for the default 'true'
  255. // state.
  256. if (uidl.hasAttribute("wordwrap")
  257. && uidl.getBooleanAttribute("wordwrap") == false) {
  258. setWordwrap(false);
  259. } else {
  260. setWordwrap(true);
  261. }
  262. }
  263. private void updateFieldContent(final String text) {
  264. setPrompting(inputPrompt != null && focusedTextField != this
  265. && (text.equals("")));
  266. String fieldValue;
  267. if (prompting) {
  268. fieldValue = isReadOnly() ? "" : inputPrompt;
  269. addStyleDependentName(CLASSNAME_PROMPT);
  270. } else {
  271. fieldValue = text;
  272. removeStyleDependentName(CLASSNAME_PROMPT);
  273. }
  274. setText(fieldValue);
  275. lastTextChangeString = valueBeforeEdit = text;
  276. valueBeforeEditIsSynced = true;
  277. }
  278. protected void onCut() {
  279. if (listenTextChangeEvents) {
  280. deferTextChangeEvent();
  281. }
  282. }
  283. protected native void attachCutEventListener(Element el)
  284. /*-{
  285. var me = this;
  286. el.oncut = function() {
  287. me.@com.vaadin.terminal.gwt.client.ui.VTextField::onCut()();
  288. };
  289. }-*/;
  290. protected native void detachCutEventListener(Element el)
  291. /*-{
  292. el.oncut = null;
  293. }-*/;
  294. @Override
  295. protected void onDetach() {
  296. super.onDetach();
  297. detachCutEventListener(getElement());
  298. if (focusedTextField == this) {
  299. focusedTextField = null;
  300. }
  301. }
  302. @Override
  303. protected void onAttach() {
  304. super.onAttach();
  305. if (listenTextChangeEvents) {
  306. detachCutEventListener(getElement());
  307. }
  308. }
  309. private void setMaxLength(int newMaxLength) {
  310. if (newMaxLength >= 0) {
  311. maxLength = newMaxLength;
  312. if (getElement().getTagName().toLowerCase().equals("textarea")) {
  313. // NOP no maxlength property for textarea
  314. } else {
  315. getElement().setPropertyInt("maxLength", maxLength);
  316. }
  317. } else if (maxLength != -1) {
  318. if (getElement().getTagName().toLowerCase().equals("textarea")) {
  319. // NOP no maxlength property for textarea
  320. } else {
  321. getElement().removeAttribute("maxLength");
  322. }
  323. maxLength = -1;
  324. }
  325. }
  326. protected int getMaxLength() {
  327. return maxLength;
  328. }
  329. public void onChange(ChangeEvent event) {
  330. valueChange(false);
  331. }
  332. /**
  333. * Called when the field value might have changed and/or the field was
  334. * blurred. These are combined so the blur event is sent in the same batch
  335. * as a possible value change event (these are often connected).
  336. *
  337. * @param blurred
  338. * true if the field was blurred
  339. */
  340. public void valueChange(boolean blurred) {
  341. if (client != null && id != null) {
  342. boolean sendBlurEvent = false;
  343. boolean sendValueChange = false;
  344. if (blurred && client.hasEventListeners(this, EventId.BLUR)) {
  345. sendBlurEvent = true;
  346. client.updateVariable(id, EventId.BLUR, "", false);
  347. }
  348. String newText = getText();
  349. if (!prompting && newText != null
  350. && !newText.equals(valueBeforeEdit)) {
  351. sendValueChange = immediate;
  352. client.updateVariable(id, "text", getText(), false);
  353. valueBeforeEdit = newText;
  354. valueBeforeEditIsSynced = true;
  355. }
  356. /*
  357. * also send cursor position, no public api yet but for easier
  358. * extension
  359. */
  360. updateCursorPosition();
  361. if (sendBlurEvent || sendValueChange) {
  362. /*
  363. * Avoid sending text change event as we will simulate it on the
  364. * server side before value change events.
  365. */
  366. textChangeEventTrigger.cancel();
  367. scheduled = false;
  368. client.sendPendingVariableChanges();
  369. }
  370. }
  371. }
  372. /**
  373. * Updates the cursor position variable if it has changed since the last
  374. * update.
  375. *
  376. * @return true iff the value was updated
  377. */
  378. protected boolean updateCursorPosition() {
  379. if (Util.isAttachedAndDisplayed(this)) {
  380. int cursorPos = getCursorPos();
  381. if (lastCursorPos != cursorPos) {
  382. client.updateVariable(id, VAR_CURSOR, cursorPos, false);
  383. lastCursorPos = cursorPos;
  384. return true;
  385. }
  386. }
  387. return false;
  388. }
  389. private static VTextField focusedTextField;
  390. public static void flushChangesFromFocusedTextField() {
  391. if (focusedTextField != null) {
  392. focusedTextField.onChange(null);
  393. }
  394. }
  395. public void onFocus(FocusEvent event) {
  396. addStyleDependentName(CLASSNAME_FOCUS);
  397. if (prompting) {
  398. setText("");
  399. removeStyleDependentName(CLASSNAME_PROMPT);
  400. setPrompting(false);
  401. }
  402. focusedTextField = this;
  403. if (client.hasEventListeners(this, EventId.FOCUS)) {
  404. client.updateVariable(id, EventId.FOCUS, "", true);
  405. }
  406. }
  407. public void onBlur(BlurEvent event) {
  408. removeStyleDependentName(CLASSNAME_FOCUS);
  409. focusedTextField = null;
  410. String text = getText();
  411. setPrompting(inputPrompt != null && (text == null || "".equals(text)));
  412. if (prompting) {
  413. setText(isReadOnly() ? "" : inputPrompt);
  414. addStyleDependentName(CLASSNAME_PROMPT);
  415. }
  416. valueChange(true);
  417. }
  418. private void setPrompting(boolean prompting) {
  419. this.prompting = prompting;
  420. }
  421. public void setColumns(int columns) {
  422. setColumns(getElement(), columns);
  423. }
  424. private native void setColumns(Element e, int c)
  425. /*-{
  426. try {
  427. switch(e.tagName.toLowerCase()) {
  428. case "input":
  429. //e.size = c;
  430. e.style.width = c+"em";
  431. break;
  432. case "textarea":
  433. //e.cols = c;
  434. e.style.width = c+"em";
  435. break;
  436. default:;
  437. }
  438. } catch (e) {}
  439. }-*/;
  440. /**
  441. * @return space used by components paddings and borders
  442. */
  443. private int getExtraHorizontalPixels() {
  444. if (extraHorizontalPixels < 0) {
  445. detectExtraSizes();
  446. }
  447. return extraHorizontalPixels;
  448. }
  449. /**
  450. * @return space used by components paddings and borders
  451. */
  452. private int getExtraVerticalPixels() {
  453. if (extraVerticalPixels < 0) {
  454. detectExtraSizes();
  455. }
  456. return extraVerticalPixels;
  457. }
  458. /**
  459. * Detects space used by components paddings and borders. Used when
  460. * relational size are used.
  461. */
  462. private void detectExtraSizes() {
  463. Element clone = Util.cloneNode(getElement(), false);
  464. DOM.setElementAttribute(clone, "id", "");
  465. DOM.setStyleAttribute(clone, "visibility", "hidden");
  466. DOM.setStyleAttribute(clone, "position", "absolute");
  467. // due FF3 bug set size to 10px and later subtract it from extra pixels
  468. DOM.setStyleAttribute(clone, "width", "10px");
  469. DOM.setStyleAttribute(clone, "height", "10px");
  470. DOM.appendChild(DOM.getParent(getElement()), clone);
  471. extraHorizontalPixels = DOM.getElementPropertyInt(clone, "offsetWidth") - 10;
  472. extraVerticalPixels = DOM.getElementPropertyInt(clone, "offsetHeight") - 10;
  473. DOM.removeChild(DOM.getParent(getElement()), clone);
  474. }
  475. @Override
  476. public void setHeight(String height) {
  477. if (height.endsWith("px")) {
  478. int h = Integer.parseInt(height.substring(0, height.length() - 2));
  479. h -= getExtraVerticalPixels();
  480. if (h < 0) {
  481. h = 0;
  482. }
  483. super.setHeight(h + "px");
  484. } else {
  485. super.setHeight(height);
  486. }
  487. }
  488. @Override
  489. public void setWidth(String width) {
  490. if (width.endsWith("px")) {
  491. int w = Integer.parseInt(width.substring(0, width.length() - 2));
  492. w -= getExtraHorizontalPixels();
  493. if (w < 0) {
  494. w = 0;
  495. }
  496. super.setWidth(w + "px");
  497. } else {
  498. super.setWidth(width);
  499. }
  500. }
  501. public void onBeforeShortcutAction(Event e) {
  502. valueChange(false);
  503. }
  504. // Here for backward compatibility; to be moved to TextArea
  505. public void setWordwrap(boolean enabled) {
  506. if (enabled == wordwrap) {
  507. return; // No change
  508. }
  509. if (enabled) {
  510. getElement().removeAttribute("wrap");
  511. getElement().getStyle().clearOverflow();
  512. } else {
  513. getElement().setAttribute("wrap", "off");
  514. getElement().getStyle().setOverflow(Overflow.AUTO);
  515. }
  516. if (BrowserInfo.get().isSafari4()) {
  517. // Force redraw as Safari 4 does not properly update the screen
  518. Util.forceWebkitRedraw(getElement());
  519. } else if (BrowserInfo.get().isOpera()) {
  520. // Opera fails to dynamically update the wrap attribute so we detach
  521. // and reattach the whole TextArea.
  522. Util.detachAttach(getElement());
  523. }
  524. wordwrap = enabled;
  525. }
  526. public void onKeyDown(KeyDownEvent event) {
  527. if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
  528. valueChange(false);
  529. }
  530. }
  531. public Widget getWidgetForPaintable() {
  532. return this;
  533. }
  534. }