001package arez.dom;
002
003import akasha.EventListener;
004import akasha.MediaQueryList;
005import akasha.Window;
006import akasha.WindowGlobal;
007import arez.ComputableValue;
008import arez.Disposable;
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 * An observable model that indicates whether a window matches a CSS media query.
023 *
024 * <p>A very simple example</p>
025 * <pre>{@code
026 * import arez.Arez;
027 * import arez.mediaquery.MediaQuery;
028 * import com.google.gwt.core.client.EntryPoint;
029 * import akasha.Console;
030 *
031 * public class MediaQueryExample
032 *   implements EntryPoint
033 * {
034 *   public void onModuleLoad()
035 *   {
036 *     final MediaQuery mediaQuery = MediaQuery.create( "(max-width: 600px)" );
037 *     Arez.context().observer( () ->
038 *                                DomGlobal.document.querySelector( "#status" ).textContent =
039 *                                  "Screen size Status: " + ( mediaQuery.matches() ? "Narrow" : "Wide" ) );
040 *   }
041 * }
042 * }</pre>
043 */
044@ArezComponent( requireId = Feature.DISABLE )
045public abstract class MediaQuery
046{
047  @Nonnull
048  private final EventListener _listener = e -> notifyOnMatchChange();
049  @Nonnull
050  private final Window _window;
051  @Nonnull
052  private MediaQueryList _mediaQueryList;
053  private boolean _active;
054
055  /**
056   * Create an instance of MediaQuery.
057   *
058   * @param query the CSS media query to match.
059   * @return the MediaQuery instance.
060   */
061  @Nonnull
062  public static MediaQuery create( @Nonnull final String query )
063  {
064    return create( WindowGlobal.window(), query );
065  }
066
067  /**
068   * Create an instance of MediaQuery.
069   *
070   * @param window the window to test.
071   * @param query  the CSS media query to match.
072   * @return the MediaQuery instance.
073   */
074  @Nonnull
075  public static MediaQuery create( @Nonnull final Window window, @Nonnull final String query )
076  {
077    return new Arez_MediaQuery( window, query );
078  }
079
080  MediaQuery( @Nonnull final Window window, @Nonnull final String query )
081  {
082    _window = Objects.requireNonNull( window );
083    _mediaQueryList = _window.matchMedia( Objects.requireNonNull( query ) );
084  }
085
086  /**
087   * Return the window against which the MediaQuery will run.
088   *
089   * @return the window to test.
090   */
091  @Nonnull
092  public Window getWindow()
093  {
094    return _window;
095  }
096
097  /**
098   * Return the media query to test against window.
099   *
100   * @return the associated media query.
101   */
102  @Nonnull
103  @Observable
104  public String getQuery()
105  {
106    return _mediaQueryList.media();
107  }
108
109  /**
110   * Change the media query to test against.
111   * If the component is active then invoking this will ensure that the listener is updated to listen
112   * to new query.
113   *
114   * @param query the CSS media query.
115   */
116  public void setQuery( @Nonnull final String query )
117  {
118    if ( _active )
119    {
120      unbindListener();
121    }
122    _mediaQueryList = _window.matchMedia( Objects.requireNonNull( query ) );
123    if ( _active )
124    {
125      bindListener();
126    }
127  }
128
129  /**
130   * Return true if the media query matches, false otherwise.
131   *
132   * @return true if the media query matches, false otherwise.
133   */
134  @Memoize( depType = DepType.AREZ_OR_EXTERNAL )
135  public boolean matches()
136  {
137    // Observe query so that this is re-calculated if query changes
138    getQuery();
139    return _mediaQueryList.matches();
140  }
141
142  @ComputableValueRef
143  abstract ComputableValue<?> getMatchesComputableValue();
144
145  @OnActivate
146  void onMatchesActivate()
147  {
148    _active = true;
149    bindListener();
150  }
151
152  @OnDeactivate
153  void onMatchesDeactivate()
154  {
155    _active = false;
156    unbindListener();
157  }
158
159  @Action
160  void notifyOnMatchChange()
161  {
162    // According to notes in https://github.com/ReactTraining/react-media/blob/master/modules/MediaQueryList.js
163    // Safari doesn't clear up listener with removeListener when the listener is already waiting in the event queue.
164    // This code makes sure Having an active flag to make sure the change is not reported after computable is
165    // deactivated and component is disposed.
166    if ( !Disposable.isDisposed( this ) )
167    {
168      getMatchesComputableValue().reportPossiblyChanged();
169    }
170  }
171
172  private void bindListener()
173  {
174    _mediaQueryList.addListener( _listener );
175  }
176
177  private void unbindListener()
178  {
179    _mediaQueryList.removeListener( _listener );
180  }
181}