001package arez.dom;
002
003import akasha.Global;
004import akasha.HashChangeEvent;
005import akasha.HashChangeEventListener;
006import akasha.Location;
007import akasha.WindowGlobal;
008import arez.ComputableValue;
009import arez.annotations.Action;
010import arez.annotations.ArezComponent;
011import arez.annotations.ComputableValueRef;
012import arez.annotations.DepType;
013import arez.annotations.Feature;
014import arez.annotations.Memoize;
015import arez.annotations.Observable;
016import arez.annotations.OnActivate;
017import arez.annotations.OnDeactivate;
018import java.util.Objects;
019import javax.annotation.Nonnull;
020
021/**
022 * This is a simple abstraction over browser location as a hash.
023 * The model exposes the observable values for the location as the application sees it via
024 * {@link #getLocation()}, the way the browser sees it via {@link #getBrowserLocation()}.
025 * The application code should define an observer that monitors the location as the browser
026 * sees it and update the location as the application sees it via {@link #changeLocation(String)}
027 * if the browser location is valid. Otherwise the browser location should be reset to the application
028 * location.
029 *
030 * <p>It should be noted that this class is not a router but a primitive that can be used to
031 * implement a router. Observing the application location will allow the application to update
032 * the view. Observing the browser location will allow the application to decide whether the
033 * route should be updated.</p>
034 */
035@ArezComponent( requireId = Feature.DISABLE )
036public abstract class BrowserLocation
037{
038  @Nonnull
039  private final HashChangeEventListener _listener = this::onHashChangeEvent;
040  /**
041   * The location according to the application.
042   */
043  @Nonnull
044  private String _location;
045  /**
046   * The location that the application is attempting to update the browser to.
047   */
048  @Nonnull
049  private String _targetLocation;
050  /**
051   * Should we prevent the default action associated with hash change?
052   */
053  private boolean _preventDefault = true;
054
055  /**
056   * Create the model object.
057   *
058   * @return the BrowserLocation instance.
059   */
060  @Nonnull
061  public static BrowserLocation create()
062  {
063    return new Arez_BrowserLocation();
064  }
065
066  BrowserLocation()
067  {
068    _targetLocation = _location = getHash();
069  }
070
071  /**
072   * Return true if component will prevent default actions when hash.
073   *
074   * @return true if component will prevent default actions when hash.
075   */
076  public boolean shouldPreventDefault()
077  {
078    return _preventDefault;
079  }
080
081  /**
082   * Set a flag to determine whether events default action will be prevented.
083   *
084   * @param preventDefault true to prevent default action.
085   */
086  public void setPreventDefault( final boolean preventDefault )
087  {
088    _preventDefault = preventDefault;
089  }
090
091  /**
092   * Change the target location to the specified parameter.
093   * This will ultimately result in a side-effect that updates the browsers location.
094   * This location parameter should not include "#" as the first character.
095   *
096   * @param targetLocation the location to change to.
097   */
098  @Action( verifyRequired = false )
099  public void changeLocation( @Nonnull final String targetLocation )
100  {
101    _targetLocation = targetLocation;
102    if ( targetLocation.equals( getBrowserLocation() ) )
103    {
104      setLocation( targetLocation );
105    }
106    setHash( targetLocation );
107    /*
108     * setHash does not trigger a "hashchange" event so explicitly call the hook here
109     */
110    updateBrowserLocation();
111  }
112
113  /**
114   * Revert the browsers location to the application location.
115   */
116  @Action
117  public void resetBrowserLocation()
118  {
119    changeLocation( getLocation() );
120  }
121
122  /**
123   * Return the location as the application sees it.
124   * This return value does not include a "#" as the first character.
125   *
126   * @return the location.
127   */
128  @Observable
129  @Nonnull
130  public String getLocation()
131  {
132    return _location;
133  }
134
135  @Observable
136  void setLocation( @Nonnull final String location )
137  {
138    _location = Objects.requireNonNull( location );
139  }
140
141  @Memoize( depType = DepType.AREZ_OR_EXTERNAL )
142  @Nonnull
143  public String getBrowserLocation()
144  {
145    return getHash();
146  }
147
148  @OnActivate
149  void onBrowserLocationActivate()
150  {
151    WindowGlobal.addHashchangeListener( _listener, false );
152  }
153
154  @OnDeactivate
155  void onBrowserLocationDeactivate()
156  {
157    WindowGlobal.removeHashchangeListener( _listener, false );
158  }
159
160  @ComputableValueRef
161  abstract ComputableValue<?> getBrowserLocationComputableValue();
162
163  @Action
164  void updateBrowserLocation()
165  {
166    getBrowserLocationComputableValue().reportPossiblyChanged();
167    final String location = getBrowserLocation();
168    if ( _targetLocation.equals( location ) )
169    {
170      setLocation( location );
171    }
172  }
173
174  private void onHashChangeEvent( @Nonnull final HashChangeEvent e )
175  {
176    if ( _preventDefault )
177    {
178      e.preventDefault();
179    }
180    updateBrowserLocation();
181  }
182
183  @Nonnull
184  private String getHash()
185  {
186    return WindowGlobal.location().hash.substring( 1 );
187  }
188
189  private void setHash( @Nonnull final String hash )
190  {
191    final Location location = WindowGlobal.location();
192    if ( 0 == hash.length() )
193    {
194      /*
195       * This code is needed to remove the stray #.
196       * See https://stackoverflow.com/questions/1397329/how-to-remove-the-hash-from-window-location-url-with-javascript-without-page-r/5298684#5298684
197       */
198      WindowGlobal.history().pushState( "", WindowGlobal.document().title, location.pathname + location.search );
199    }
200    else
201    {
202      location.hash = hash;
203    }
204  }
205}