| package org.eclipse.osbp.runtime.web.vaadin.components.fields.client; |
| |
| 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.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.client.ApplicationConnection; |
| import com.vaadin.client.BrowserInfo; |
| import com.vaadin.client.Util; |
| import com.vaadin.client.ui.Field; |
| import com.vaadin.shared.EventId; |
| import com.vaadin.shared.ui.textfield.TextFieldConstants; |
| |
| public class VEntityTextField extends TextBoxBase implements Field, |
| ChangeHandler, FocusHandler, BlurHandler, KeyDownHandler { |
| |
| /** |
| * The input node CSS classname. |
| */ |
| public static final String CLASSNAME = "v-entitytextfield"; |
| /** |
| * This CSS classname is added to the input node on hover. |
| */ |
| public static final String CLASSNAME_FOCUS = "focus"; |
| |
| /** For internal use only. May be removed or replaced in the future. */ |
| public String paintableId; |
| |
| /** For internal use only. May be removed or replaced in the future. */ |
| public ApplicationConnection client; |
| |
| /** For internal use only. May be removed or replaced in the future. */ |
| public 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 maxLength = -1; |
| |
| private static final String CLASSNAME_PROMPT = "prompt"; |
| private static final String TEXTCHANGE_MODE_TIMEOUT = "TIMEOUT"; |
| |
| private String inputPrompt = null; |
| private boolean prompting = false; |
| private int lastCursorPos = -1; |
| |
| public VEntityTextField() { |
| this(DOM.createInputText()); |
| } |
| |
| protected VEntityTextField(Element node) { |
| super(node); |
| 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); |
| } |
| |
| /** |
| * For internal use only. May be removed or replaced in the future. |
| * <p> |
| * TODO When GWT adds ONCUT, add it there and remove workaround. See |
| * http://code.google.com/p/google-web-toolkit/issues/detail?id=4030 |
| * <p> |
| * 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. |
| */ |
| public static final int TEXTCHANGE_EVENTS = Event.ONPASTE | Event.KEYEVENTS |
| | Event.ONMOUSEUP; |
| |
| @Override |
| public void onBrowserEvent(Event event) { |
| super.onBrowserEvent(event); |
| |
| 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(paintableId, |
| TextFieldConstants.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; |
| |
| /** For internal use only. May be removed or replaced in the future. */ |
| public boolean listenTextChangeEvents; |
| /** For internal use only. May be removed or replaced in the future. */ |
| public String textChangeEventMode; |
| public 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); |
| } |
| |
| /** For internal use only. May be removed or replaced in the future. */ |
| public 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; |
| valueBeforeEditIsSynced = true; |
| } |
| |
| protected void onCut() { |
| if (listenTextChangeEvents) { |
| deferTextChangeEvent(); |
| } |
| } |
| |
| /** For internal use only. May be removed or replaced in the future. */ |
| public native void attachCutEventListener(Element el) |
| /*-{ |
| var me = this; |
| el.oncut = $entry(function() { |
| me.@com.vaadin.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()); |
| } |
| } |
| |
| /** For internal use only. May be removed or replaced in the future. */ |
| public 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"); |
| |
| } |
| setMaxLengthToElement(maxLength); |
| } |
| |
| protected void setMaxLengthToElement(int newMaxLength) { |
| if (newMaxLength >= 0) { |
| getElement().setPropertyInt("maxLength", newMaxLength); |
| } else { |
| getElement().removeAttribute("maxLength"); |
| } |
| } |
| |
| public int getMaxLength() { |
| return maxLength; |
| } |
| |
| @Override |
| 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 && paintableId != null) { |
| boolean sendBlurEvent = false; |
| boolean sendValueChange = false; |
| |
| if (blurred && client.hasEventListeners(this, EventId.BLUR)) { |
| sendBlurEvent = true; |
| client.updateVariable(paintableId, EventId.BLUR, "", false); |
| } |
| |
| String newText = getText(); |
| if (!prompting && newText != null |
| && !newText.equals(valueBeforeEdit)) { |
| sendValueChange = immediate; |
| client.updateVariable(paintableId, "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(paintableId, |
| TextFieldConstants.VAR_CURSOR, cursorPos, false); |
| lastCursorPos = cursorPos; |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private static VEntityTextField focusedTextField; |
| |
| public static void flushChangesFromFocusedTextField() { |
| if (focusedTextField != null) { |
| focusedTextField.onChange(null); |
| } |
| } |
| |
| @Override |
| public void onFocus(FocusEvent event) { |
| addStyleDependentName(CLASSNAME_FOCUS); |
| if (prompting) { |
| setText(""); |
| removeStyleDependentName(CLASSNAME_PROMPT); |
| setPrompting(false); |
| } |
| focusedTextField = this; |
| if (client != null && client.hasEventListeners(this, EventId.FOCUS)) { |
| client.updateVariable(paintableId, EventId.FOCUS, "", true); |
| } |
| } |
| |
| @Override |
| 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) { |
| if (columns <= 0) { |
| return; |
| } |
| |
| setWidth(columns + "em"); |
| } |
| |
| @Override |
| public void onKeyDown(KeyDownEvent event) { |
| if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) { |
| valueChange(false); |
| } |
| } |
| |
| public void setImmediate(boolean immediate) { |
| this.immediate = immediate; |
| } |
| |
| public void setInputPrompt(String inputPrompt) { |
| this.inputPrompt = inputPrompt; |
| } |
| |
| protected boolean isWordwrap() { |
| String wrap = getElement().getAttribute("wrap"); |
| return !"off".equals(wrap); |
| } |
| } |