// Copyright 2012 Google Inc. All Rights Reserved. // // 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.collide.client.history; import com.google.collide.clientlibs.navigation.NavigationToken; import com.google.collide.clientlibs.navigation.UrlSerializationController; import com.google.collide.json.client.JsoArray; import com.google.collide.json.shared.JsonArray; import com.google.common.annotations.VisibleForTesting; import elemental.client.Browser; import elemental.events.Event; import elemental.events.EventListener; /* * TODO: The prefixing of paths with "/h/" is to allow us to use HTML5 * pushState(). We currently still use the hash fragment because our testing * infrastructure doesn't work with FF4, or Chrome. Rage. But we are setup to * easily switch to using it once the testing infrastructure gets with the * times. */ /** * Utility class for extracting String encodings of {@link Place}s from the * history string, and for creating entries in History based on * {@link PlaceNavigationEvent}s. * * Note that because we are using HTML5 pushState() and popState(), that we are * not using a hash fragment to encode the history string. We use a simple URL * scheme where the path of the URL is the History String. * * In order to not collide with some of the reserved URL mappings exposed by * the Frontend, we prefix paths used as history with "/h/", since the FE treats * "/h/*" URLs like a request for the root servlet. * */ public class HistoryUtils { /** * Callback for entities that are interested when HistoryUtils API changes the * URL. */ public interface SetHistoryListener { void onHistorySet(String historyString); } /** * This gets called back when the user navigates history via back/forward * button presses. This does NOT get dispatched when we set the history token * ourselves. */ public interface ValueChangeListener { void onValueChanged(String historyString); } private static String lastSetHistoryString = ""; private static final JsoArray<SetHistoryListener> setHistoryListeners = JsoArray.create(); private static final JsoArray<ValueChangeListener> valueChangeListeners = JsoArray.create(); private static final UrlSerializationController urlSerializationController = new UrlSerializationController(); // We want to trap changes to the hash fragment. static { Browser.getWindow().addEventListener("hashchange", new EventListener() { @Override public void handleEvent(Event evt) { String currentHistoryString = getHistoryString(); // We dispatch only if the current history string is different from one // that we set. if (!lastSetHistoryString.equals(currentHistoryString)) { for (int i = 0, n = valueChangeListeners.size(); i < n; i++) { valueChangeListeners.get(i).onValueChanged(currentHistoryString); } } lastSetHistoryString = currentHistoryString; } }, false); } /** * Adds a listener that will be called whenever the URL changes. This gets * called each time we set a history token. It will also be called immediately * from this registration. */ public static void addSetHistoryListener(SetHistoryListener listener) { setHistoryListeners.add(listener); listener.onHistorySet(getHistoryString()); } /** * Adds a listener that will be called whenever the user presses back and * forward. This will NOT get called when we set the history string. */ public static void addValueChangeListener(ValueChangeListener listener) { valueChangeListeners.add(listener); } /** * Takes in a snapshot of the active Places, and creates a History entry for * it. */ public static void createHistoryEntry(JsoArray<? extends NavigationToken> historySnapshot) { String historyString = createHistoryString(historySnapshot); // This will update the URL without refreshing the browser. setHistoryString(historyString); // Now inform interested parties of the new URL. for (int i = 0, n = setHistoryListeners.size(); i < n; i++) { setHistoryListeners.get(i).onHistorySet(historyString); } } public static String createHistoryString(JsonArray<? extends NavigationToken> historySnapshot) { return urlSerializationController.serializeToUrl(historySnapshot); } /** * @return the currently set, entire History String. */ public static String getHistoryString() { // TODO: We're currently using hash when we refactor the place // framework we will move to pushstate. String hashFragment = Browser.getWindow().getLocation().getHash(); // Remove the hash/ if (hashFragment.length() > 0) { hashFragment = hashFragment.substring(1); } return Browser.decodeURI(hashFragment); } /** * See: {@link #parseHistoryString(String historyString)}. */ public static JsonArray<NavigationToken> parseHistoryString() { return parseHistoryString(getHistoryString()); } /** * Parses the history string and returns an array of {@link HistoryPiece}s. * These can be used to construct an array of {@link PlaceNavigationEvent}s * that can be fired on the {@link RootPlace}. * * @param historyString the String corresponding to the entire History Token. * @return the parsed history String as an array of history pieces, or an * empty {@link JsoArray} if the History String is malformed or not * present. */ public static JsonArray<NavigationToken> parseHistoryString(String historyString) { return urlSerializationController.deserializeFromUrl(historyString); } @VisibleForTesting static void setHistoryString(String historyString) { lastSetHistoryString = historyString; // TODO: When we move to FF4, we can use the pushState() API. // Until then, we are stuck with the hash fragment. // history.pushState(null, null, historyString); Browser.getWindow().getLocation().setHash(Browser.encodeURI(historyString)); } }