/* * Copyright 2010 Traction Software, Inc. * * 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.tractionsoftware.gwt.user.client.ui; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.event.dom.client.HasKeyDownHandlers; import com.google.gwt.event.dom.client.HasKeyUpHandlers; import com.google.gwt.event.dom.client.KeyCodeEvent; 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.event.dom.client.KeyUpEvent; import com.google.gwt.event.dom.client.KeyUpHandler; import com.google.gwt.event.logical.shared.HasValueChangeHandlers; import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Focusable; import com.google.gwt.user.client.ui.HasText; import com.google.gwt.user.client.ui.HasValue; import com.google.gwt.user.client.ui.Widget; import com.tractionsoftware.gwt.user.client.util.MiscUtils; /** * This attaches to an input, listening for KeyDown/KeyUp, and * automatically resizing the text area. It does this using a shadow * control that matches the text of the input. */ public abstract class AutoSizingBase<T extends Widget & HasTextSelection & HasValue<String> & HasValueChangeHandlers<String> & HasKeyDownHandlers & HasKeyUpHandlers & Focusable & HasText, S extends Widget> extends Composite implements KeyDownHandler, KeyUpHandler, ValueChangeHandler<String>, // for PillList Focusable, HasText { public static final int DEFAULT_MAX = 10000; public static final int DEFAULT_MIN = 0; // ---------------------------------------------------------------------- // abstract methods for subclass /** * Returns the size of the shadow element */ public abstract int getShadowSize(); /** * @param text the text that should be set on the shadow to * determine the appropriate size of the widget */ public abstract void setShadowText(String text); /** * @param size will take into account minSize, maxSize, and * extraSize. the implementation should just call setWidth or * setHeight as appropriate. */ public abstract void setSize(int size); // ---------------------------------------------------------------------- // shared configuration // size is either width or height depending on the control // (TextBox vs. TextArea) public final int getMinSize() { return minSize; } public final void setMinSize(int minSize) { this.minSize = minSize; } public final int getMaxSize() { return maxSize; } public final void setMaxSize(int maxSize) { this.maxSize = maxSize; } /** * This is the amount of extra horizontal or vertical space that * will be added. */ public final int getExtraSize() { return extraSize; } public final void setExtraSize(int extraSize) { this.extraSize = extraSize; } // ---------------------------------------------------------------------- // check for max-height and min-height properties and use them // instead of anything configured. if you need to control the // min/max in code, don't set those css properties. // // also note that we remove the properties from the textarea AND // the shadow. this is important because otherwise they interfere // with the auto-sizing // public final void setMinFromCss(String property) { int min = getAndResetValueFromCss(property, "0"); if (min > 0) { setMinSize(min); } } public final void setMaxFromCss(String property) { int max = getAndResetValueFromCss(property, "none"); if (max > 0) { setMaxSize(max); } } public final int getAndResetValueFromCss(String property, String reset) { int value = MiscUtils.getComputedStyleInt(box.getElement(), property); if (value > 0) { box.getElement().getStyle().setProperty(property, reset); shadow.getElement().getStyle().setProperty(property, reset); } return value; } // ---------------------------------------------------------------------- protected int minSize = DEFAULT_MIN; protected int maxSize = DEFAULT_MAX; protected int extraSize; protected final T box; protected final S shadow; protected final FlowPanel div = new FlowPanel(); public AutoSizingBase(T box, S shadow) { this.box = box; this.shadow = shadow; box.addKeyDownHandler(this); box.addKeyUpHandler(this); box.addValueChangeHandler(this); div.setStyleName("gwt-traction-input-autosize"); shadow.setStyleName("gwt-traction-input-shadow"); // make sure the shadow isn't in the tab order if (shadow instanceof Focusable) { // we can't use -1 because FocusWidget.onAttach looks for // that and sets it to 0. any negative value will remove // it from the tab order. ((Focusable) shadow).setTabIndex(-2); } // note this has to be in a FlowPanel to work div.add(box); div.add(shadow); initWidget(div); } /** * Matches the styles and adjusts the size. This needs to be * called after the input is added to the DOM, so we do it in * onLoad. */ @Override protected void onLoad() { super.onLoad(); // these styles need to be the same for the box and shadow so // that we can measure properly matchStyles("display"); matchStyles("fontSize"); matchStyles("fontFamily"); matchStyles("fontWeight"); matchStyles("lineHeight"); matchStyles("paddingTop"); matchStyles("paddingRight"); matchStyles("paddingBottom"); matchStyles("paddingLeft"); adjustSize(); } @Override public T getWidget() { return box; } // ---------------------------------------------------------------------- // style manipulation public void matchStyles(String name) { String value = MiscUtils.getComputedStyle(box.getElement(), name); if (value != null) { try { // we might have a bogus value (e.g. width: -10px). we // just let it fail quietly. shadow.getElement().getStyle().setProperty(name, value); } catch (Exception e) { GWT.log("Exception in matchStyles for name="+name+" value="+value, e); } } } public void setStyles(String name, String value) { box.getElement().getStyle().setProperty(name, value); shadow.getElement().getStyle().setProperty(name, value); } // ---------------------------------------------------------------------- // event handling code /** * On key down we assume the key will go at the end. It's the most * common case and not that distracting if that's not true. */ @Override public void onKeyDown(KeyDownEvent event) { char c = MiscUtils.getCharCode(event.getNativeEvent()); onKeyCodeEvent(event, box.getValue()+c); } @Override public void onKeyUp(KeyUpEvent event) { onKeyCodeEvent(event, box.getValue()); } protected void onKeyCodeEvent(KeyCodeEvent event, String newShadowText) { // ignore arrow keys switch (event.getNativeKeyCode()) { case KeyCodes.KEY_UP: case KeyCodes.KEY_DOWN: case KeyCodes.KEY_LEFT: case KeyCodes.KEY_RIGHT: break; default: // don't do this if there's a selection because it will get smaller if (box.getSelectionLength() == 0) { setShadowText(newShadowText); adjustSize(); break; } } } @Override public void onValueChange(ValueChangeEvent<String> event) { // here, we just match them and adjust the size again. this // will handle backspace and typing over a selection. sync(); } public void sync() { setShadowText(box.getValue()); adjustSize(); } // ---------------------------------------------------------------------- // the meat (not very meaty) public void resetSize() { setSize(Math.max(minSize, extraSize)); } public void adjustSize() { int size = getShadowSize() + extraSize; if (size < minSize) { size = minSize; } else if (size > maxSize) { size = maxSize; } setSize(size); } public void setWidth(int width) { box.setWidth(width+"px"); div.setWidth(width+"px"); } public void setHeight(int height) { box.setHeight(height+"px"); div.setHeight(height+"px"); } // ---------------------------------------------------------------------- // Focusable (proxy to SuggestBox) @Override public final int getTabIndex() { return box.getTabIndex(); } @Override public final void setTabIndex(int index) { box.setTabIndex(index); } @Override public final void setFocus(boolean focus) { if (focus) { Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { box.setFocus(true); } }); } else { box.setFocus(false); } } @Override public final void setAccessKey(char key) { box.setAccessKey(key); } // ---------------------------------------------------------------------- // HasText @Override public final String getText() { return box.getText(); } @Override public abstract void setText(String text); }