| /* |
| * Copyright 2008 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.client; |
| |
| import com.google.gwt.core.client.GWT; |
| 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.event.shared.GwtEvent; |
| import com.google.gwt.event.shared.HandlerManager; |
| import com.google.gwt.event.shared.HandlerRegistration; |
| |
| /** |
| * This class allows you to interact with the browser's history stack. Each |
| * "item" on the stack is represented by a single string, referred to as a |
| * "token". You can create new history items (which have a token associated with |
| * them when they are created), and you can programmatically force the current |
| * history to move back or forward. |
| * |
| * <p> |
| * In order to receive notification of user-directed changes to the current |
| * history item, implement the {@link ValueChangeHandler} interface and attach |
| * it via {@link #addValueChangeHandler(ValueChangeHandler)}. |
| * </p> |
| * |
| * <p> |
| * <h3>Example</h3> |
| * {@example com.google.gwt.examples.HistoryExample} |
| * </p> |
| * |
| * <p> |
| * <h3>URL Encoding</h3> |
| * Any valid characters may be used in the history token and will survive |
| * round-trips through {@link #newItem(String)} to {@link #getToken()}/ |
| * {@link ValueChangeHandler#onValueChange(com.google.gwt.event.logical.shared.ValueChangeEvent)} |
| * , but most will be encoded in the user-visible URL. The following US-ASCII |
| * characters are not encoded on any currently supported browser (but may be in |
| * the future due to future browser changes): |
| * <ul> |
| * <li>a-z |
| * <li>A-Z |
| * <li>0-9 |
| * <li>;,/?:@&=+$-_.!~*() |
| * </ul> |
| * </p> |
| */ |
| public class History { |
| |
| private static class HistoryEventSource implements HasValueChangeHandlers<String> { |
| |
| private HandlerManager handlers = new HandlerManager(null); |
| |
| @Override |
| public void fireEvent(GwtEvent<?> event) { |
| handlers.fireEvent(event); |
| } |
| |
| @Override |
| public HandlerRegistration addValueChangeHandler(ValueChangeHandler<String> handler) { |
| return handlers.addHandler(ValueChangeEvent.getType(), handler); |
| } |
| |
| public void fireValueChangedEvent(String newToken) { |
| ValueChangeEvent.fire(this, newToken); |
| } |
| |
| public HandlerManager getHandlers() { |
| return handlers; |
| } |
| } |
| |
| /** |
| * HistoryTokenEncoder is responsible for encoding and decoding history token, |
| * thus ensuring that tokens are safe to use in the browsers URL. |
| */ |
| private static class HistoryTokenEncoder { |
| public native String encode(String toEncode) /*-{ |
| // encodeURI() does *not* encode the '#' character. |
| return $wnd.encodeURI(toEncode).replace("#", "%23"); |
| }-*/; |
| |
| public native String decode(String toDecode) /*-{ |
| return $wnd.decodeURI(toDecode.replace("%23", "#")); |
| }-*/; |
| } |
| |
| /** |
| * NoopHistoryTokenEncoder does not perform any encoding. |
| */ |
| // Used from rebinding |
| @SuppressWarnings("unused") |
| private static class NoopHistoryTokenEncoder extends HistoryTokenEncoder { |
| @Override |
| public String encode(String toEncode) { |
| return toEncode; |
| } |
| |
| @Override |
| public String decode(String toDecode) { |
| return toDecode; |
| } |
| } |
| |
| /** |
| * History implementation using hash tokens. |
| * <p>This is the default implementation for all browsers. |
| */ |
| private static class HistoryImpl { |
| |
| public HistoryImpl() { |
| attachListener(); |
| } |
| |
| protected native void attachListener() /*-{ |
| // We explicitly use the third parameter for capture, since Firefox before version 6 |
| // throws an exception if the parameter is missing. |
| // See: https://developer.mozilla.org/es/docs/DOM/elemento.addEventListener#Gecko_notes |
| var handler = $entry(@History::onHashChanged()); |
| $wnd.addEventListener('hashchange', handler, false); |
| }-*/; |
| |
| public native void newToken(String historyToken) /*-{ |
| $wnd.location.hash = historyToken; |
| }-*/; |
| |
| public void replaceToken(String historyToken) { |
| Window.Location.replace("#" + historyToken); |
| } |
| } |
| |
| @SuppressWarnings("deprecation") |
| private static class WrapHistory extends BaseListenerWrapper<HistoryListener> |
| implements ValueChangeHandler<String> { |
| @Deprecated |
| public static void add(HistoryListener listener) { |
| addValueChangeHandler(new WrapHistory(listener)); |
| } |
| |
| public static void remove(HandlerManager manager, HistoryListener listener) { |
| baseRemove(manager, listener, ValueChangeEvent.getType()); |
| } |
| |
| private WrapHistory(HistoryListener listener) { |
| super(listener); |
| } |
| |
| @Override |
| public void onValueChange(ValueChangeEvent<String> event) { |
| listener.onHistoryChanged(event.getValue()); |
| } |
| } |
| |
| private static HistoryImpl impl = GWT.create(HistoryImpl.class); |
| private static HistoryEventSource historyEventSource = new HistoryEventSource(); |
| private static HistoryTokenEncoder tokenEncoder = GWT.create(HistoryTokenEncoder.class); |
| private static String token = getDecodedHash(); |
| |
| /** |
| * Adds a listener to be informed of changes to the browser's history stack. |
| * |
| * @param listener the listener to be added |
| * @deprecated use {@link History#addValueChangeHandler(ValueChangeHandler)} instead |
| */ |
| @Deprecated |
| public static void addHistoryListener(HistoryListener listener) { |
| WrapHistory.add(listener); |
| } |
| |
| /** |
| * Adds a {@link com.google.gwt.event.logical.shared.ValueChangeEvent} handler |
| * to be informed of changes to the browser's history stack. |
| * |
| * @param handler the handler |
| * @return the registration used to remove this value change handler |
| */ |
| public static HandlerRegistration addValueChangeHandler( |
| ValueChangeHandler<String> handler) { |
| return historyEventSource.addValueChangeHandler(handler); |
| } |
| |
| /** |
| * Programmatic equivalent to the user pressing the browser's 'back' button. |
| */ |
| public static native void back() /*-{ |
| $wnd.history.back(); |
| }-*/; |
| |
| /** |
| * Encode a history token for use as part of a URI. |
| * |
| * @param historyToken the token to encode |
| * @return the encoded token, suitable for use as part of a URI |
| */ |
| public static String encodeHistoryToken(String historyToken) { |
| return tokenEncoder.encode(historyToken); |
| } |
| |
| /** |
| * Fire |
| * {@link ValueChangeHandler#onValueChange(com.google.gwt.event.logical.shared.ValueChangeEvent)} |
| * events with the current history state. This is most often called at the end |
| * of an application's |
| * {@link com.google.gwt.core.client.EntryPoint#onModuleLoad()} to inform |
| * history handlers of the initial application state. |
| */ |
| public static void fireCurrentHistoryState() { |
| String currentToken = getToken(); |
| historyEventSource.fireValueChangedEvent(currentToken); |
| } |
| |
| /** |
| * Programmatic equivalent to the user pressing the browser's 'forward' |
| * button. |
| */ |
| public static native void forward() /*-{ |
| $wnd.history.forward(); |
| }-*/; |
| |
| /** |
| * Gets the current history token. The handler will not receive a |
| * {@link ValueChangeHandler#onValueChange(com.google.gwt.event.logical.shared.ValueChangeEvent)} |
| * event for the initial token; requiring that an application request the |
| * token explicitly on startup gives it an opportunity to run different |
| * initialization code in the presence or absence of an initial token. |
| * |
| * @return the initial token, or the empty string if none is present. |
| */ |
| public static String getToken() { |
| return token; |
| } |
| |
| /** |
| * Adds a new browser history entry. Calling this method will cause |
| * {@link ValueChangeHandler#onValueChange(com.google.gwt.event.logical.shared.ValueChangeEvent)} |
| * to be called as well. |
| * |
| * @param historyToken the token to associate with the new history item |
| */ |
| public static void newItem(String historyToken) { |
| newItem(historyToken, true); |
| } |
| |
| /** |
| * Adds a new browser history entry. Calling this method will cause |
| * {@link ValueChangeHandler#onValueChange(com.google.gwt.event.logical.shared.ValueChangeEvent)} |
| * to be called as well if and only if issueEvent is true. |
| * |
| * @param historyToken the token to associate with the new history item |
| * @param issueEvent true if a |
| * {@link ValueChangeHandler#onValueChange(com.google.gwt.event.logical.shared.ValueChangeEvent)} |
| * event should be issued |
| */ |
| public static void newItem(String historyToken, boolean issueEvent) { |
| historyToken = (historyToken == null) ? "" : historyToken; |
| if (!historyToken.equals(getToken())) { |
| token = historyToken; |
| String updateToken = encodeHistoryToken(historyToken); |
| impl.newToken(updateToken); |
| if (issueEvent) { |
| historyEventSource.fireValueChangedEvent(historyToken); |
| } |
| } |
| } |
| |
| /** |
| * Call all history handlers with the specified token. Note that this does not |
| * change the history system's idea of the current state and is only kept for |
| * backward compatibility. To fire history events for the initial state of the |
| * application, instead call {@link #fireCurrentHistoryState()} from the |
| * application {@link com.google.gwt.core.client.EntryPoint#onModuleLoad()} |
| * method. |
| * |
| * @param historyToken history token to fire events for |
| * @deprecated Use {@link #fireCurrentHistoryState()} instead. |
| */ |
| @Deprecated |
| public static void onHistoryChanged(String historyToken) { |
| historyEventSource.fireValueChangedEvent(historyToken); |
| } |
| |
| /** |
| * Removes a history listener. |
| * |
| * @param listener the listener to be removed |
| */ |
| @Deprecated |
| public static void removeHistoryListener(HistoryListener listener) { |
| WrapHistory.remove(historyEventSource.getHandlers(), listener); |
| } |
| |
| /** |
| * Replace the current history token on top of the browsers history stack. |
| * |
| * <p>Note: This method has problems. The URL is updated with window.location.replace, |
| * this unfortunately has side effects when using the deprecated iframe linker |
| * (ie. "std" linker). Make sure you are using the cross site iframe linker when using |
| * this method in your code. |
| * |
| * <p>Calling this method will cause |
| * {@link ValueChangeHandler#onValueChange(com.google.gwt.event.logical.shared.ValueChangeEvent)} |
| * to be called as well. |
| * |
| * @param historyToken history token to replace current top entry |
| */ |
| public static void replaceItem(String historyToken) { |
| replaceItem(historyToken, true); |
| } |
| |
| /** |
| * Replace the current history token on top of the browsers history stack. |
| * |
| * <p>Note: This method has problems. The URL is updated with window.location.replace, |
| * this unfortunately has side effects when using the deprecated iframe linker |
| * (ie. "std" linker). Make sure you are using the cross site iframe linker when using |
| * this method in your code. |
| * |
| * <p>Calling this method will cause |
| * {@link ValueChangeHandler#onValueChange(com.google.gwt.event.logical.shared.ValueChangeEvent)} |
| * to be called as well if and only if issueEvent is true. |
| * |
| * @param historyToken history token to replace current top entry |
| * @param issueEvent issueEvent true if a |
| * {@link ValueChangeHandler#onValueChange(com.google.gwt.event.logical.shared.ValueChangeEvent)} |
| * event should be issued |
| */ |
| public static void replaceItem(String historyToken, boolean issueEvent) { |
| token = historyToken; |
| impl.replaceToken(encodeHistoryToken(historyToken)); |
| if (issueEvent) { |
| fireCurrentHistoryState(); |
| } |
| } |
| |
| private static String getDecodedHash() { |
| String hashToken = Window.Location.getHash(); |
| if (hashToken == null || hashToken.isEmpty()) { |
| return ""; |
| } |
| return tokenEncoder.decode(hashToken.substring(1)); |
| } |
| |
| // this is called from JS when the native onhashchange occurs |
| private static void onHashChanged() { |
| /* |
| * We guard against firing events twice, some browser (e.g. safari) tend to |
| * fire events on startup if HTML5 pushstate is used. |
| */ |
| String hashToken = getDecodedHash(); |
| if (!hashToken.equals(getToken())) { |
| token = hashToken; |
| historyEventSource.fireValueChangedEvent(hashToken); |
| } |
| } |
| } |