blob: ef96fd54dc72af409f6bb87ff406d989468df31d [file] [log] [blame]
/*
* 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);
}
}
}