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}