/*
* Copyright 2012 Johannes Barop
*
* 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 org.jboss.errai.ui.nav.client.local.pushstate;
import java.util.logging.Logger;
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;
import com.google.gwt.logging.client.LogConfiguration;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.Window;
/**
* Enhances GWT's History implementation to add HTML5 pushState support.
*
* <p>
* This class no longer inherits from HistoryImpl to allow for compatibility
* with both GWT 2.6 and GWT 2.7+. HistoryImpl was moved in GWT 2.7 and is no
* longer accessible. The previously inherited methods are now part of this
* class.
* </p>
*
* <p>
* The complete path is treated as history token.
* </p>
*
* <p>
* The leading '/' is hidden from GWTs History API, so that the path '/' is
* returned as an empty history token ('').
* </p>
*
* @author <a href="mailto:jb@barop.de">Johannes Barop</a>
* @author Christian Sadilek <csadilek@redhat.com>
*/
public class HistoryImplPushState implements HasValueChangeHandlers<String> {
private static final Logger LOG = Logger.getLogger(HistoryImplPushState.class.getName());
private HandlerManager handlers = new HandlerManager(null);
private String token = "";
public boolean init() {
// initialize HistoryImpl with the current path
updateHistoryToken(Window.Location.getPath() + Window.Location.getQueryString());
// initialize the empty state with the current history token
nativeUpdate(token, true);
// initialize the popState handler
initPopStateHandler();
return true;
}
private void nativeUpdate(final String historyToken) {
nativeUpdate(historyToken, false);
}
private void nativeUpdate(final String historyToken, boolean replace) {
String newPushStateToken = CodeServerParameterHelper.append(encodeFragment(historyToken));
if (!newPushStateToken.startsWith("/")) {
newPushStateToken = "/" + newPushStateToken;
}
if(replace){
replaceState(newPushStateToken);
if (LogConfiguration.loggingIsEnabled()) {
LOG.fine("Replaced '" + newPushStateToken + "' (" + historyToken + ")");
}
}else{
pushState(newPushStateToken);
if (LogConfiguration.loggingIsEnabled()) {
LOG.fine("Pushed '" + newPushStateToken + "' (" + historyToken + ")");
}
}
}
/**
* Set the current path as GWT History token which can later retrieved with
* {@link History#getToken()}.
*/
private void updateHistoryToken(String path) {
String[] split = path.split("\\?");
String token = split[0];
token = (token.length() > 0) ? decodeFragment(token) : "";
token = (token.startsWith("/")) ? token.substring(1) : token;
String queryString = (split.length == 2) ? split[1] : "";
queryString = CodeServerParameterHelper.remove(queryString);
if (queryString != null && !queryString.trim().isEmpty()) {
token += "?" + queryString;
}
if (LogConfiguration.loggingIsEnabled()) {
LOG.fine("Set token to '" + token + "'");
}
this.token = token;
}
/**
* Initialize an event handler that gets executed when the token changes.
*/
private native void initPopStateHandler() /*-{
var that = this;
var oldHandler = $wnd.onpopstate;
$wnd.onpopstate = $entry(function(e) {
if (e.state && e.state.historyToken) {
that.@org.jboss.errai.ui.nav.client.local.pushstate.HistoryImplPushState::onPopState(Ljava/lang/String;)(e.state.historyToken);
}
if (oldHandler) {
oldHandler();
}
});
}-*/;
/**
* Called from native JavaScript when an old history state was popped.
*/
private void onPopState(final String historyToken) {
if (LogConfiguration.loggingIsEnabled()) {
LOG.fine("Popped '" + historyToken + "'");
}
updateHistoryToken(historyToken);
fireHistoryChangedImpl(token);
}
/**
* Add the given token to the history using pushState.
*/
private static native void pushState(final String token) /*-{
var state = {
historyToken : token
};
$wnd.history.pushState(state, $doc.title, token);
}-*/;
/**
* Replace the given token in the history using replaceState.
*/
private static native void replaceState(final String token) /*-{
var state = {
historyToken : token
};
$wnd.history.replaceState(state, $doc.title, token);
}-*/;
private native String encodeFragment(String fragment) /*-{
// encodeURI() does *not* encode the '#' character.
return encodeURI(fragment).replace("#", "%23");
}-*/;
private native String decodeFragment(String encodedFragment) /*-{
// decodeURI() does *not* decode the '#' character.
return decodeURI(encodedFragment.replace("%23", "#"));
}-*/;
/**
* Fires the {@link ValueChangeEvent} to all handlers with the given tokens.
*/
public void fireHistoryChangedImpl(String newToken) {
ValueChangeEvent.fire(this, newToken);
}
/**
* Fires the {@link ValueChangeEvent} to all handlers with the current token.
*/
public void fireCurrentHistoryState() {
ValueChangeEvent.fire(this, token);
}
@Override
public void fireEvent(GwtEvent<?> event) {
handlers.fireEvent(event);
}
/**
* Adds a {@link ValueChangeEvent} handler to be informed of changes to the
* browser's history stack.
*
* @param handler the handler
*/
public HandlerRegistration addValueChangeHandler(
ValueChangeHandler<String> handler) {
return handlers.addHandler(ValueChangeEvent.getType(), handler);
}
/**
* 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 final void newItem(String historyToken, boolean issueEvent) {
historyToken = (historyToken == null) ? "" : historyToken;
if (!historyToken.equals(this.token)) {
this.token = historyToken;
nativeUpdate(historyToken);
if (issueEvent) {
fireHistoryChangedImpl(historyToken);
}
}
}
}