001package arez.dom;
002
003import arez.ComputableValue;
004import arez.Disposable;
005import arez.annotations.Action;
006import arez.annotations.ArezComponent;
007import arez.annotations.ComputableValueRef;
008import arez.annotations.DepType;
009import arez.annotations.Feature;
010import arez.annotations.Memoize;
011import arez.annotations.Observable;
012import arez.annotations.OnActivate;
013import arez.annotations.OnDeactivate;
014import java.util.Objects;
015import javax.annotation.Nonnull;
016import jsinterop.annotations.JsFunction;
017import jsinterop.annotations.JsMethod;
018import jsinterop.annotations.JsPackage;
019import jsinterop.annotations.JsProperty;
020import jsinterop.annotations.JsType;
021
022/**
023 * An observable model that indicates whether a window matches a CSS media query.
024 *
025 * <p>A very simple example</p>
026 * <pre>{@code
027 * import arez.Arez;
028 * import arez.dom.MediaQuery;
029 * import com.google.gwt.core.client.EntryPoint;
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 *                                System.out.println( "Screen size Status: " +
039 *                                                    ( mediaQuery.matches() ? "Narrow" : "Wide" ) ) );
040 *   }
041 * }
042 * }</pre>
043 */
044@ArezComponent( requireId = Feature.DISABLE )
045public abstract class MediaQuery
046{
047  /**
048   * Minimal facade for the browser window media-query API.
049   */
050  @JsType( isNative = true, name = "Window", namespace = JsPackage.GLOBAL )
051  public static class Window
052  {
053    protected Window()
054    {
055    }
056
057    @JsMethod
058    native MediaQueryList matchMedia( String query );
059  }
060
061  @JsFunction
062  private interface EventListener
063  {
064    void handleEvent( Object event );
065  }
066
067  @JsType( isNative = true, name = "MediaQueryList", namespace = JsPackage.GLOBAL )
068  private static class MediaQueryList
069  {
070    @JsProperty( name = "media" )
071    native String media();
072
073    @JsProperty( name = "matches" )
074    native boolean matches();
075
076    @JsMethod
077    native void addListener( EventListener listener );
078
079    @JsMethod
080    native void removeListener( EventListener listener );
081  }
082
083  @Nonnull
084  private final EventListener _listener = e -> notifyOnMatchChange();
085  @Nonnull
086  private final Window _window;
087  @Nonnull
088  private MediaQueryList _mediaQueryList;
089  private boolean _active;
090
091  /**
092   * Create an instance of MediaQuery.
093   *
094   * @param query the CSS media query to match.
095   * @return the MediaQuery instance.
096   */
097  @Nonnull
098  public static MediaQuery create( @Nonnull final String query )
099  {
100    return create( window(), query );
101  }
102
103  /**
104   * Create an instance of MediaQuery.
105   *
106   * @param window the window to test.
107   * @param query  the CSS media query to match.
108   * @return the MediaQuery instance.
109   */
110  @Nonnull
111  public static MediaQuery create( @Nonnull final Window window, @Nonnull final String query )
112  {
113    return new Arez_MediaQuery( window, query );
114  }
115
116  MediaQuery( @Nonnull final Window window, @Nonnull final String query )
117  {
118    _window = Objects.requireNonNull( window );
119    _mediaQueryList = _window.matchMedia( Objects.requireNonNull( query ) );
120  }
121
122  /**
123   * Return the window against which the MediaQuery will run.
124   *
125   * @return the window to test.
126   */
127  @Nonnull
128  public Window getWindow()
129  {
130    return _window;
131  }
132
133  /**
134   * Return the media query to test against window.
135   *
136   * @return the associated media query.
137   */
138  @Nonnull
139  @Observable
140  public String getQuery()
141  {
142    return _mediaQueryList.media();
143  }
144
145  /**
146   * Change the media query to test against.
147   * If the component is active then invoking this will ensure that the listener is updated to listen
148   * to new query.
149   *
150   * @param query the CSS media query.
151   */
152  public void setQuery( @Nonnull final String query )
153  {
154    if ( _active )
155    {
156      unbindListener();
157    }
158    _mediaQueryList = _window.matchMedia( Objects.requireNonNull( query ) );
159    if ( _active )
160    {
161      bindListener();
162    }
163  }
164
165  /**
166   * Return true if the media query matches, false otherwise.
167   *
168   * @return true if the media query matches, false otherwise.
169   */
170  @Memoize( depType = DepType.AREZ_OR_EXTERNAL )
171  public boolean matches()
172  {
173    // Observe query so that this is re-calculated if query changes
174    getQuery();
175    return _mediaQueryList.matches();
176  }
177
178  @ComputableValueRef
179  abstract ComputableValue<?> getMatchesComputableValue();
180
181  @OnActivate
182  void onMatchesActivate()
183  {
184    _active = true;
185    bindListener();
186  }
187
188  @OnDeactivate
189  void onMatchesDeactivate()
190  {
191    _active = false;
192    unbindListener();
193  }
194
195  @Action
196  void notifyOnMatchChange()
197  {
198    // According to notes in https://github.com/ReactTraining/react-media/blob/master/modules/MediaQueryList.js
199    // Safari doesn't clear up listener with removeListener when the listener is already waiting in the event queue.
200    // This code makes sure Having an active flag to make sure the change is not reported after computable is
201    // deactivated and component is disposed.
202    if ( !Disposable.isDisposed( this ) )
203    {
204      getMatchesComputableValue().reportPossiblyChanged();
205    }
206  }
207
208  private void bindListener()
209  {
210    _mediaQueryList.addListener( _listener );
211  }
212
213  private void unbindListener()
214  {
215    _mediaQueryList.removeListener( _listener );
216  }
217
218  @JsProperty( name = "window", namespace = JsPackage.GLOBAL )
219  private static native Window window();
220}