| /* |
| * Copyright 2010 Google 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.google.gwt.user.cellview.client; |
| |
| import com.google.gwt.core.client.GWT; |
| import com.google.gwt.core.client.JavaScriptObject; |
| import com.google.gwt.core.client.Scheduler; |
| import com.google.gwt.core.client.Scheduler.ScheduledCommand; |
| import com.google.gwt.core.shared.impl.StringCase; |
| import com.google.gwt.dom.client.BrowserEvents; |
| import com.google.gwt.dom.client.Document; |
| import com.google.gwt.dom.client.Element; |
| import com.google.gwt.dom.client.EventTarget; |
| import com.google.gwt.dom.client.InputElement; |
| import com.google.gwt.safehtml.shared.SafeHtml; |
| import com.google.gwt.safehtml.shared.SafeHtmlUtils; |
| import com.google.gwt.user.client.DOM; |
| import com.google.gwt.user.client.Event; |
| import com.google.gwt.user.client.EventListener; |
| import com.google.gwt.user.client.ui.Widget; |
| |
| import java.util.HashSet; |
| import java.util.Set; |
| |
| /** |
| * IE specified Impl used by cell based widgets. |
| */ |
| class CellBasedWidgetImplTrident extends CellBasedWidgetImpl { |
| |
| /** |
| * The method used to dispatch non-bubbling events. |
| */ |
| private static JavaScriptObject dispatchFocusEvent; |
| |
| /** |
| * The currently focused input element, select box, or text area. |
| */ |
| private static Element focusedInput; |
| |
| /** |
| * If true, only synthesize change events when the focused input is blurred. |
| */ |
| private static boolean focusedInputChangesOnBlurOnly; |
| |
| /** |
| * The last value of the focused input element. |
| */ |
| private static Object focusedInputValue; |
| |
| /** |
| * The set of input types that can receive change events. |
| */ |
| private static Set<String> inputTypes; |
| |
| /** |
| * Dispatch an event to the cell, ensuring that the widget will catch it. |
| * |
| * @param widget the widget that will handle the event |
| * @param target the cell element |
| * @param eventBits the event bits to sink |
| * @param event the event to fire, or null not to fire an event |
| */ |
| private static void dispatchCellEvent(Widget widget, Element target, |
| int eventBits, Event event) { |
| // Make sure that the target is still a child of the widget. We defer the |
| // firing of some events, so its possible that the DOM structure has |
| // changed before we fire the event. |
| if (!widget.getElement().isOrHasChild(target)) { |
| return; |
| } |
| |
| // Temporary listen for events from the cell. The event listener will be |
| // removed in onBrowserEvent(). |
| DOM.setEventListener(target, widget); |
| DOM.sinkEvents(target, eventBits | DOM.getEventsSunk(target)); |
| |
| // Dispatch the event to the cell. |
| if (event != null) { |
| target.dispatchEvent(event); |
| } |
| } |
| |
| /** |
| * Get the value of an element that has a value or checked state. |
| * |
| * @param elem the input element |
| * @return the value of the input |
| */ |
| private static Object getInputValue(Element elem) { |
| if (isCheckbox(elem)) { |
| return InputElement.as(elem).isChecked(); |
| } |
| return getInputValueImpl(elem); |
| } |
| |
| /** |
| * Get the value of an element that has a value, such as an input element, |
| * textarea, or select box. |
| * |
| * @param elem the input element |
| * @return the value of the input |
| */ |
| private static native String getInputValueImpl(Element elem) /*-{ |
| return elem.value; |
| }-*/; |
| |
| /** |
| * Used by {@link #initFocusEventSystem()} and {@link #initLoadEvents(String)} |
| * to handle non bubbling events . |
| * |
| * @param event |
| */ |
| @SuppressWarnings("unused") |
| private static void handleNonBubblingEvent(Event event) { |
| // Get the event target. |
| EventTarget eventTarget = event.getEventTarget(); |
| if (!Element.is(eventTarget)) { |
| return; |
| } |
| final Element target = eventTarget.cast(); |
| |
| // Get the event listener. |
| Element curElem = target; |
| EventListener listener = DOM.getEventListener(curElem); |
| while (curElem != null && listener == null) { |
| curElem = curElem.getParentElement(); |
| listener = (curElem == null) ? null : DOM.getEventListener(curElem); |
| } |
| |
| // Get the Widget from the event listener. |
| if (!(listener instanceof Widget)) { |
| return; |
| } |
| Widget widget = (Widget) listener; |
| |
| // Do not special case events that occur on the widget itself. |
| if (target == widget.getElement()) { |
| return; |
| } |
| |
| String type = event.getType(); |
| if (BrowserEvents.FOCUSIN.equals(type)) { |
| // If this is an input element, remember that we focused it. |
| String tagName = StringCase.toLower(target.getTagName()); |
| if (inputTypes.contains(tagName)) { |
| focusedInput = target; |
| focusedInputValue = getInputValue(target); |
| focusedInputChangesOnBlurOnly = !"select".equals(tagName) |
| && !isCheckbox(target); |
| } |
| |
| // The focus event has not fired yet, so we just need to set the |
| // CellTable as the event listener and wait for it. |
| dispatchCellEvent(widget, target, Event.ONFOCUS, null); |
| } else if (BrowserEvents.FOCUSOUT.equals(type)) { |
| // Fire a change event on the input element if the value changed. |
| maybeFireChangeEvent(widget); |
| focusedInput = null; |
| |
| // The blur event has already fired, so we need to synthesize one. |
| Event blurEvent = Document.get().createFocusEvent().cast(); |
| dispatchCellEvent(widget, target, Event.ONBLUR, null); |
| } else if (BrowserEvents.LOAD.equals(type) || BrowserEvents.ERROR.equals(type)) { |
| DOM.dispatchEvent(event, widget.getElement(), listener); |
| } |
| } |
| |
| /** |
| * Check whether or not an element is a checkbox or radio button. |
| * |
| * @param elem the element to check |
| * @return true if a checkbox, false if not |
| */ |
| private static boolean isCheckbox(Element elem) { |
| if (elem == null || !"input".equalsIgnoreCase(elem.getTagName())) { |
| return false; |
| } |
| String inputType = StringCase.toLower(InputElement.as(elem).getType()); |
| return "checkbox".equals(inputType) || "radio".equals(inputType); |
| } |
| |
| /** |
| * Synthesize a change event on the focused input element if the value has |
| * changed. |
| * |
| * @param widget the {@link Widget} containing the element |
| */ |
| private static void maybeFireChangeEvent(Widget widget) { |
| if (focusedInput == null) { |
| return; |
| } |
| |
| Object newValue = getInputValue(focusedInput); |
| if (!newValue.equals(focusedInputValue)) { |
| // Save the new value in case it changes again. |
| focusedInputValue = newValue; |
| |
| // Fire a synthetic event to the input element. |
| Element target = focusedInput; |
| Event changeEvent = Document.get().createChangeEvent().cast(); |
| dispatchCellEvent(widget, target, Event.ONCHANGE, changeEvent); |
| } |
| } |
| |
| /** |
| * The set of event types that can trigger a change event. |
| */ |
| private final Set<String> changeEventTriggers; |
| |
| /** |
| * If true, load events have been initialized. |
| */ |
| private boolean loadEventsInitialized; |
| |
| public CellBasedWidgetImplTrident() { |
| // Initialize the input types. |
| if (inputTypes == null) { |
| inputTypes = new HashSet<String>(); |
| inputTypes.add("select"); |
| inputTypes.add("input"); |
| inputTypes.add("textarea"); |
| } |
| |
| // Initialize the change event triggers. |
| changeEventTriggers = new HashSet<String>(); |
| changeEventTriggers.add(BrowserEvents.MOUSEUP); |
| changeEventTriggers.add(BrowserEvents.MOUSEWHEEL); |
| } |
| |
| @Override |
| public boolean isFocusable(Element elem) { |
| return focusableTypes.contains(StringCase.toLower(elem.getTagName())) |
| || getTabIndexIfSpecified(elem) >= 0; |
| } |
| |
| @Override |
| public void onBrowserEvent(final Widget widget, Event event) { |
| // We need to remove the event listener from the cell now that the event |
| // has fired. |
| String type = StringCase.toLower(event.getType()); |
| if (BrowserEvents.FOCUS.equals(type) || BrowserEvents.BLUR.equals(type) || BrowserEvents.CHANGE.equals(type)) { |
| EventTarget eventTarget = event.getEventTarget(); |
| if (Element.is(eventTarget)) { |
| Element target = eventTarget.cast(); |
| if (target != widget.getElement()) { |
| DOM.setEventListener(target, null); |
| } |
| } |
| } |
| |
| // Update the value of the focused input box. |
| if (focusedInput != null && BrowserEvents.CHANGE.equals(type)) { |
| focusedInputValue = getInputValue(focusedInput); |
| } |
| |
| // We might need to fire a synthetic change event on the input element. |
| if (focusedInput != null && !focusedInputChangesOnBlurOnly |
| && changeEventTriggers.contains(type)) { |
| // Defer the change event because the change does not occur until after |
| // the events specified above. |
| Scheduler.get().scheduleDeferred(new ScheduledCommand() { |
| public void execute() { |
| maybeFireChangeEvent(widget); |
| } |
| }); |
| } |
| } |
| |
| @Override |
| public SafeHtml processHtml(SafeHtml html) { |
| // If the widget is listening for load events, we modify the HTML to add the |
| // load/error listeners. |
| if (loadEventsInitialized && html != null) { |
| String moduleName = GWT.getModuleName(); |
| String listener = "__gwt_CellBasedWidgetImplLoadListeners[\"" |
| + moduleName + "\"]();"; |
| |
| String htmlString = html.asString(); |
| htmlString = htmlString.replaceAll("(<img)([\\s/>])", "<img onload='" |
| + listener + "' onerror='" + listener + "'$2"); |
| |
| // We assert that the resulting string is safe |
| html = SafeHtmlUtils.fromTrustedString(htmlString); |
| } |
| return html; |
| } |
| |
| @Override |
| public void resetFocus(ScheduledCommand command) { |
| // IE will not focus an element that was created in this event loop. |
| Scheduler.get().scheduleDeferred(command); |
| } |
| |
| @Override |
| protected int sinkEvent(Widget widget, String typeName) { |
| if (BrowserEvents.CHANGE.equals(typeName) || BrowserEvents.FOCUS.equals(typeName) |
| || BrowserEvents.BLUR.equals(typeName)) { |
| // Initialize the focus events. |
| if (dispatchFocusEvent == null) { |
| initFocusEventSystem(); |
| } |
| |
| // Sink the events required for focus. We use an attribute on the widget |
| // to remember whether or not we've sunk the events. |
| int eventsToSink = 0; |
| Element elem = widget.getElement(); |
| String attr = "__gwtCellBasedWidgetImplDispatchingFocus"; |
| if (!"true".equals(elem.getAttribute(attr))) { |
| elem.setAttribute(attr, "true"); |
| sinkFocusEvents(elem); |
| |
| // Sink the events that could trigger a change event. Change events |
| // are also triggered on blur if the value changes. |
| for (String trigger : changeEventTriggers) { |
| eventsToSink |= Event.getTypeInt(trigger); |
| } |
| } |
| return eventsToSink; |
| } else if (BrowserEvents.LOAD.equals(typeName) || BrowserEvents.ERROR.equals(typeName)) { |
| // Initialize the load listener. |
| if (!loadEventsInitialized) { |
| loadEventsInitialized = true; |
| initLoadEvents(GWT.getModuleName()); |
| } |
| return -1; |
| } else { |
| return super.sinkEvent(widget, typeName); |
| } |
| } |
| |
| /** |
| * Get the tab index of an element if the tab index is specified. |
| * |
| * @param elem the Element |
| * @return the tab index, or -1 if not specified |
| */ |
| private native int getTabIndexIfSpecified(Element elem) /*-{ |
| var attrNode = elem.getAttributeNode('tabIndex'); |
| return (attrNode != null && attrNode.specified) ? elem.tabIndex : -1; |
| }-*/; |
| |
| /** |
| * Initialize the focus event listener. |
| */ |
| private native void initFocusEventSystem() /*-{ |
| @com.google.gwt.user.cellview.client.CellBasedWidgetImplTrident::dispatchFocusEvent = $entry(function() { |
| @com.google.gwt.user.cellview.client.CellBasedWidgetImplTrident::handleNonBubblingEvent(Lcom/google/gwt/user/client/Event;)($wnd.event); |
| }); |
| }-*/; |
| |
| /** |
| * Initialize load events. We hang a method off of $wnd so we can reference it |
| * in the HTML generated for img tags. |
| * |
| * @param moduleName the module name of the current module |
| */ |
| private native void initLoadEvents(String moduleName) /*-{ |
| // Initialize an array of listeners. Each module gets its own entry in the |
| // array to prevent conflicts on pages with multiple modules. |
| if (!$wnd.__gwt_CellBasedWidgetImplLoadListeners) { |
| $wnd.__gwt_CellBasedWidgetImplLoadListeners = new Array(); |
| } |
| |
| // Add an entry for the specified module. |
| $wnd.__gwt_CellBasedWidgetImplLoadListeners[moduleName] = $entry(function() { |
| @com.google.gwt.user.cellview.client.CellBasedWidgetImplTrident::handleNonBubblingEvent(Lcom/google/gwt/user/client/Event;)($wnd.event); |
| }); |
| }-*/; |
| |
| /** |
| * Sink focus events for the specified element. |
| * |
| * @param elem the element that will receive the events |
| */ |
| private native void sinkFocusEvents(Element elem) /*-{ |
| elem.attachEvent('onfocusin', |
| @com.google.gwt.user.cellview.client.CellBasedWidgetImplTrident::dispatchFocusEvent); |
| elem.attachEvent('onfocusout', |
| @com.google.gwt.user.cellview.client.CellBasedWidgetImplTrident::dispatchFocusEvent); |
| }-*/; |
| } |