/* @ITMillApache2LicenseForJavaFiles@ */ 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 IT Mill 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; 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 boolean communicateTextValueToServer() { String text = getText(); if (prompting) { // Input prompt visible, text is actually "" text = ""; } if (!text.equals(getLastCommunicatedString())) { lastTextChangeString = text; client.updateVariable(id, VAR_CUR_TEXT, text, true); return true; } return false; } private Timer textChangeEventTrigger = new Timer() { @Override public void run() { if (isAttached()) { updateCursorPosition(); boolean textChanged = communicateTextValueToServer(); if (textChanged) { client.sendPendingVariableChanges(); } 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 (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(""))); String fieldValue; if (prompting) { fieldValue = isReadOnly() ? "" : inputPrompt; addStyleDependentName(CLASSNAME_PROMPT); } else { fieldValue = text; removeStyleDependentName(CLASSNAME_PROMPT); } setText(fieldValue); lastTextChangeString = valueBeforeEdit = text; } protected void onCut() { if (listenTextChangeEvents) { deferTextChangeEvent(); } } protected native void attachCutEventListener(Element el) /*-{ var me = this; el.oncut = 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 >= 0) { maxLength = newMaxLength; if (getElement().getTagName().toLowerCase().equals("textarea")) { // NOP no maxlength property for textarea } else { getElement().setPropertyInt("maxLength", maxLength); } } else if (maxLength != -1) { if (getElement().getTagName().toLowerCase().equals("textarea")) { // NOP no maxlength property for textarea } else { getElement().removeAttribute("maxLength"); } maxLength = -1; } } 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", getText(), false); valueBeforeEdit = newText; } /* * 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); } focusedTextField = this; if (client.hasEventListeners(this, EventId.FOCUS)) { client.updateVariable(client.getPid(this), EventId.FOCUS, "", true); } } public void onBlur(BlurEvent event) { 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); } } }