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 18KB

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