/* * Copyright 2011 Vaadin Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.vaadin.terminal.gwt.client.ui; import com.google.gwt.core.client.Scheduler; import com.google.gwt.dom.client.Style.Overflow; import com.google.gwt.event.dom.client.BlurEvent; import com.google.gwt.event.dom.client.BlurHandler; import com.google.gwt.event.dom.client.ChangeEvent; import com.google.gwt.event.dom.client.ChangeHandler; import com.google.gwt.event.dom.client.FocusEvent; import com.google.gwt.event.dom.client.FocusHandler; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyDownEvent; import com.google.gwt.event.dom.client.KeyDownHandler; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.TextBoxBase; import com.vaadin.terminal.gwt.client.ApplicationConnection; import com.vaadin.terminal.gwt.client.BrowserInfo; import com.vaadin.terminal.gwt.client.EventId; import com.vaadin.terminal.gwt.client.Paintable; import com.vaadin.terminal.gwt.client.UIDL; import com.vaadin.terminal.gwt.client.Util; import com.vaadin.terminal.gwt.client.VTooltip; import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.BeforeShortcutActionListener; /** * This class represents a basic text input field with one row. * * @author Vaadin Ltd. * */ public class VTextField extends TextBoxBase implements Paintable, Field, ChangeHandler, FocusHandler, BlurHandler, BeforeShortcutActionListener, KeyDownHandler { public static final String VAR_CUR_TEXT = "curText"; public static final String ATTR_NO_VALUE_CHANGE_BETWEEN_PAINTS = "nvc"; /** * The input node CSS classname. */ public static final String CLASSNAME = "v-textfield"; /** * This CSS classname is added to the input node on hover. */ public static final String CLASSNAME_FOCUS = "focus"; protected String id; protected ApplicationConnection client; private String valueBeforeEdit = null; /** * Set to false if a text change event has been sent since the last value * change event. This means that {@link #valueBeforeEdit} should not be * trusted when determining whether a text change even should be sent. */ private boolean valueBeforeEditIsSynced = true; private boolean immediate = false; private int extraHorizontalPixels = -1; private int extraVerticalPixels = -1; private int maxLength = -1; private static final String CLASSNAME_PROMPT = "prompt"; private static final String ATTR_INPUTPROMPT = "prompt"; public static final String ATTR_TEXTCHANGE_TIMEOUT = "iet"; public static final String VAR_CURSOR = "c"; public static final String ATTR_TEXTCHANGE_EVENTMODE = "iem"; private static final String TEXTCHANGE_MODE_EAGER = "EAGER"; private static final String TEXTCHANGE_MODE_TIMEOUT = "TIMEOUT"; private String inputPrompt = null; private boolean prompting = false; private int lastCursorPos = -1; private boolean wordwrap = true; public VTextField() { this(DOM.createInputText()); } protected VTextField(Element node) { super(node); if (BrowserInfo.get().getIEVersion() > 0 && BrowserInfo.get().getIEVersion() < 8) { // Fixes IE margin problem (#2058) DOM.setStyleAttribute(node, "marginTop", "-1px"); DOM.setStyleAttribute(node, "marginBottom", "-1px"); } setStyleName(CLASSNAME); addChangeHandler(this); if (BrowserInfo.get().isIE()) { // IE does not send change events when pressing enter in a text // input so we handle it using a key listener instead addKeyDownHandler(this); } addFocusHandler(this); addBlurHandler(this); sinkEvents(VTooltip.TOOLTIP_EVENTS); } /* * TODO When GWT adds ONCUT, add it there and remove workaround. See * http://code.google.com/p/google-web-toolkit/issues/detail?id=4030 * * Also note that the cut/paste are not totally crossbrowsers compatible. * E.g. in Opera mac works via context menu, but on via File->Paste/Cut. * Opera might need the polling method for 100% working textchanceevents. * Eager polling for a change is bit dum and heavy operation, so I guess we * should first try to survive without. */ private static final int TEXTCHANGE_EVENTS = Event.ONPASTE | Event.KEYEVENTS | Event.ONMOUSEUP; @Override public void onBrowserEvent(Event event) { super.onBrowserEvent(event); if (client != null) { client.handleTooltipEvent(event, this); } if (listenTextChangeEvents && (event.getTypeInt() & TEXTCHANGE_EVENTS) == event .getTypeInt()) { deferTextChangeEvent(); } } /* * TODO optimize this so that only changes are sent + make the value change * event just a flag that moves the current text to value */ private String lastTextChangeString = null; private String getLastCommunicatedString() { return lastTextChangeString; } private void communicateTextValueToServer() { String text = getText(); if (prompting) { // Input prompt visible, text is actually "" text = ""; } if (!text.equals(getLastCommunicatedString())) { if (valueBeforeEditIsSynced && text.equals(valueBeforeEdit)) { /* * Value change for the current text has been enqueued since the * last text change event was sent, but we can't know that it * has been sent to the server. Ensure that all pending changes * are sent now. Sending a value change without a text change * will simulate a TextChangeEvent on the server. */ client.sendPendingVariableChanges(); } else { // Default case - just send an immediate text change message client.updateVariable(id, VAR_CUR_TEXT, text, true); // Shouldn't investigate valueBeforeEdit to avoid duplicate text // change events as the states are not in sync any more valueBeforeEditIsSynced = false; } lastTextChangeString = text; } } private Timer textChangeEventTrigger = new Timer() { @Override public void run() { if (isAttached()) { updateCursorPosition(); communicateTextValueToServer(); scheduled = false; } } }; private boolean scheduled = false; private boolean listenTextChangeEvents; private String textChangeEventMode; private int textChangeEventTimeout; private void deferTextChangeEvent() { if (textChangeEventMode.equals(TEXTCHANGE_MODE_TIMEOUT) && scheduled) { return; } else { textChangeEventTrigger.cancel(); } textChangeEventTrigger.schedule(getTextChangeEventTimeout()); scheduled = true; } private int getTextChangeEventTimeout() { return textChangeEventTimeout; } @Override public void setReadOnly(boolean readOnly) { boolean wasReadOnly = isReadOnly(); if (readOnly) { setTabIndex(-1); } else if (wasReadOnly && !readOnly && getTabIndex() == -1) { /* * Need to manually set tab index to 0 since server will not send * the tab index if it is 0. */ setTabIndex(0); } super.setReadOnly(readOnly); } public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { this.client = client; id = uidl.getId(); if (client.updateComponent(this, uidl, true)) { return; } if (focusedTextField == this) { // Re-add this here as all stylenames are overwritten by // the client.updateComponent call above (#11623) addStyleDependentName(CLASSNAME_FOCUS); } if (uidl.getBooleanAttribute("readonly")) { setReadOnly(true); } else { setReadOnly(false); } inputPrompt = uidl.getStringAttribute(ATTR_INPUTPROMPT); setMaxLength(uidl.hasAttribute("maxLength") ? uidl .getIntAttribute("maxLength") : -1); immediate = uidl.getBooleanAttribute("immediate"); listenTextChangeEvents = client.hasEventListeners(this, "ie"); if (listenTextChangeEvents) { textChangeEventMode = uidl .getStringAttribute(ATTR_TEXTCHANGE_EVENTMODE); if (textChangeEventMode.equals(TEXTCHANGE_MODE_EAGER)) { textChangeEventTimeout = 1; } else { textChangeEventTimeout = uidl .getIntAttribute(ATTR_TEXTCHANGE_TIMEOUT); if (textChangeEventTimeout < 1) { // Sanitize and allow lazy/timeout with timeout set to 0 to // work as eager textChangeEventTimeout = 1; } } sinkEvents(TEXTCHANGE_EVENTS); attachCutEventListener(getElement()); } if (uidl.hasAttribute("cols")) { setColumns(new Integer(uidl.getStringAttribute("cols")).intValue()); } final String text = uidl.getStringVariable("text"); /* * We skip the text content update if field has been repainted, but text * has not been changed. Additional sanity check verifies there is no * change in the que (in which case we count more on the server side * value). */ if (!(uidl.getBooleanAttribute(ATTR_NO_VALUE_CHANGE_BETWEEN_PAINTS) && valueBeforeEdit != null && text.equals(valueBeforeEdit))) { updateFieldContent(text); } if (uidl.hasAttribute("selpos")) { final int pos = uidl.getIntAttribute("selpos"); final int length = uidl.getIntAttribute("sellen"); /* * Gecko defers setting the text so we need to defer the selection. */ Scheduler.get().scheduleDeferred(new Command() { public void execute() { setSelectionRange(pos, length); } }); } // Here for backward compatibility; to be moved to TextArea. // Optimization: server does not send attribute for the default 'true' // state. if (uidl.hasAttribute("wordwrap") && uidl.getBooleanAttribute("wordwrap") == false) { setWordwrap(false); } else { setWordwrap(true); } } private void updateFieldContent(final String text) { setPrompting(inputPrompt != null && focusedTextField != this && (text.equals(""))); if (BrowserInfo.get().isFF3()) { /* * Firefox 3 is really sluggish when updating input attached to dom. * Some optimizations seems to work much better in Firefox3 if we * update the actual content lazily when the rest of the DOM has * stabilized. In tests, about ten times better performance is * achieved with this optimization. See for eg. #2898 */ Scheduler.get().scheduleDeferred(new Command() { public void execute() { String fieldValue; if (prompting) { fieldValue = isReadOnly() ? "" : inputPrompt; addStyleDependentName(CLASSNAME_PROMPT); } else { fieldValue = text; removeStyleDependentName(CLASSNAME_PROMPT); } /* * Avoid resetting the old value. Prevents cursor flickering * which then again happens due to this Gecko hack. */ if (!getText().equals(fieldValue)) { setText(fieldValue); } } }); } else { String fieldValue; if (prompting) { fieldValue = isReadOnly() ? "" : inputPrompt; addStyleDependentName(CLASSNAME_PROMPT); } else { fieldValue = text; removeStyleDependentName(CLASSNAME_PROMPT); } setText(fieldValue); } lastTextChangeString = valueBeforeEdit = text; valueBeforeEditIsSynced = true; } protected void onCut() { if (listenTextChangeEvents) { deferTextChangeEvent(); } } protected native void attachCutEventListener(Element el) /*-{ var me = this; el.oncut = $entry(function() { me.@com.vaadin.terminal.gwt.client.ui.VTextField::onCut()(); }); }-*/; protected native void detachCutEventListener(Element el) /*-{ el.oncut = null; }-*/; @Override protected void onDetach() { super.onDetach(); detachCutEventListener(getElement()); if (focusedTextField == this) { focusedTextField = null; } } @Override protected void onAttach() { super.onAttach(); if (listenTextChangeEvents) { detachCutEventListener(getElement()); } } private void setMaxLength(int newMaxLength) { if (newMaxLength == maxLength) { return; } maxLength = newMaxLength; updateMaxLength(maxLength); } /** * This method is responsible for updating the DOM or otherwise ensuring * that the given max length is enforced. Called when the max length for the * field has changed. * * @param maxLength * The new max length */ protected void updateMaxLength(int maxLength) { if (maxLength >= 0) { getElement().setPropertyInt("maxLength", maxLength); } else { getElement().removeAttribute("maxLength"); } } protected int getMaxLength() { return maxLength; } public void onChange(ChangeEvent event) { valueChange(false); } /** * Called when the field value might have changed and/or the field was * blurred. These are combined so the blur event is sent in the same batch * as a possible value change event (these are often connected). * * @param blurred * true if the field was blurred */ public void valueChange(boolean blurred) { if (client != null && id != null) { boolean sendBlurEvent = false; boolean sendValueChange = false; if (blurred && client.hasEventListeners(this, EventId.BLUR)) { sendBlurEvent = true; client.updateVariable(id, EventId.BLUR, "", false); } String newText = getText(); if (!prompting && newText != null && !newText.equals(valueBeforeEdit)) { sendValueChange = immediate; client.updateVariable(id, "text", newText, false); valueBeforeEdit = newText; valueBeforeEditIsSynced = true; } /* * also send cursor position, no public api yet but for easier * extension */ updateCursorPosition(); if (sendBlurEvent || sendValueChange) { /* * Avoid sending text change event as we will simulate it on the * server side before value change events. */ textChangeEventTrigger.cancel(); scheduled = false; client.sendPendingVariableChanges(); } } } /** * Updates the cursor position variable if it has changed since the last * update. * * @return true iff the value was updated */ protected boolean updateCursorPosition() { if (Util.isAttachedAndDisplayed(this)) { int cursorPos = getCursorPos(); if (lastCursorPos != cursorPos) { client.updateVariable(id, VAR_CURSOR, cursorPos, false); lastCursorPos = cursorPos; return true; } } return false; } private static VTextField focusedTextField; public static void flushChangesFromFocusedTextField() { if (focusedTextField != null) { focusedTextField.onChange(null); } } public void onFocus(FocusEvent event) { addStyleDependentName(CLASSNAME_FOCUS); if (prompting) { setText(""); removeStyleDependentName(CLASSNAME_PROMPT); setPrompting(false); if (BrowserInfo.get().isIE6()) { // IE6 does not show the cursor when tabbing into the field setCursorPos(0); } } focusedTextField = this; if (client.hasEventListeners(this, EventId.FOCUS)) { client.updateVariable(client.getPid(this), EventId.FOCUS, "", true); } } public void onBlur(BlurEvent event) { // this is called twice on Chrome when e.g. changing tab while prompting // field focused - do not change settings on the second time if (focusedTextField != this) { return; } removeStyleDependentName(CLASSNAME_FOCUS); focusedTextField = null; String text = getText(); setPrompting(inputPrompt != null && (text == null || "".equals(text))); if (prompting) { setText(isReadOnly() ? "" : inputPrompt); addStyleDependentName(CLASSNAME_PROMPT); } valueChange(true); } private void setPrompting(boolean prompting) { this.prompting = prompting; } public void setColumns(int columns) { setColumns(getElement(), columns); } private native void setColumns(Element e, int c) /*-{ try { switch(e.tagName.toLowerCase()) { case "input": //e.size = c; e.style.width = c+"em"; break; case "textarea": //e.cols = c; e.style.width = c+"em"; break; default:; } } catch (e) {} }-*/; /** * @return space used by components paddings and borders */ private int getExtraHorizontalPixels() { if (extraHorizontalPixels < 0) { detectExtraSizes(); } return extraHorizontalPixels; } /** * @return space used by components paddings and borders */ private int getExtraVerticalPixels() { if (extraVerticalPixels < 0) { detectExtraSizes(); } return extraVerticalPixels; } /** * Detects space used by components paddings and borders. Used when * relational size are used. */ private void detectExtraSizes() { Element clone = Util.cloneNode(getElement(), false); DOM.setElementAttribute(clone, "id", ""); DOM.setStyleAttribute(clone, "visibility", "hidden"); DOM.setStyleAttribute(clone, "position", "absolute"); // due FF3 bug set size to 10px and later subtract it from extra pixels DOM.setStyleAttribute(clone, "width", "10px"); DOM.setStyleAttribute(clone, "height", "10px"); DOM.appendChild(DOM.getParent(getElement()), clone); extraHorizontalPixels = DOM.getElementPropertyInt(clone, "offsetWidth") - 10; extraVerticalPixels = DOM.getElementPropertyInt(clone, "offsetHeight") - 10; DOM.removeChild(DOM.getParent(getElement()), clone); } @Override public void setHeight(String height) { if (height.endsWith("px")) { int h = Integer.parseInt(height.substring(0, height.length() - 2)); h -= getExtraVerticalPixels(); if (h < 0) { h = 0; } super.setHeight(h + "px"); } else { super.setHeight(height); } } @Override public void setWidth(String width) { if (width.endsWith("px")) { int w = Integer.parseInt(width.substring(0, width.length() - 2)); w -= getExtraHorizontalPixels(); if (w < 0) { w = 0; } super.setWidth(w + "px"); } else { super.setWidth(width); } } public void onBeforeShortcutAction(Event e) { valueChange(false); } // Here for backward compatibility; to be moved to TextArea public void setWordwrap(boolean enabled) { if (enabled == wordwrap) { return; // No change } if (enabled) { getElement().removeAttribute("wrap"); getElement().getStyle().clearOverflow(); } else { getElement().setAttribute("wrap", "off"); getElement().getStyle().setOverflow(Overflow.AUTO); } if (BrowserInfo.get().isSafari4()) { // Force redraw as Safari 4 does not properly update the screen Util.forceWebkitRedraw(getElement()); } else if (BrowserInfo.get().isOpera()) { // Opera fails to dynamically update the wrap attribute so we detach // and reattach the whole TextArea. Util.detachAttach(getElement()); } wordwrap = enabled; } public void onKeyDown(KeyDownEvent event) { if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) { valueChange(false); } } protected boolean isWordwrap() { String wrap = getElement().getAttribute("wrap"); return !"off".equals(wrap); } }