001/*
002 * #%L
003 * GwtMaterial
004 * %%
005 * Copyright (C) 2015 - 2017 GwtMaterialDesign
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 * 
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 * 
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package gwt.material.design.addins.client.masonry;
021
022import com.google.gwt.dom.client.Document;
023import com.google.gwt.event.shared.HandlerRegistration;
024import com.google.gwt.user.client.ui.IsWidget;
025import com.google.gwt.user.client.ui.Widget;
026import gwt.material.design.addins.client.MaterialAddins;
027import gwt.material.design.addins.client.base.constants.AddinsCssName;
028import gwt.material.design.addins.client.masonry.events.HasMasonryHandler;
029import gwt.material.design.addins.client.masonry.events.LayoutCompleteEvent;
030import gwt.material.design.addins.client.masonry.events.MasonryEvents;
031import gwt.material.design.addins.client.masonry.events.RemoveCompleteEvent;
032import gwt.material.design.addins.client.masonry.js.JsMasonry;
033import gwt.material.design.addins.client.masonry.js.JsMasonryOptions;
034import gwt.material.design.client.MaterialDesignBase;
035import gwt.material.design.client.base.HasDurationTransition;
036import gwt.material.design.client.base.JsLoader;
037import gwt.material.design.client.base.MaterialWidget;
038import gwt.material.design.client.constants.CssName;
039import gwt.material.design.client.ui.MaterialRow;
040
041import static gwt.material.design.addins.client.masonry.js.JsMasonry.$;
042
043//@formatter:off
044
045/**
046 * Masonry works by placing elements in optimal position based on available vertical space, sort of like a mason fitting stones in a wall.
047 * <p>
048 * <h3>XML Namespace Declaration</h3>
049 * <pre>
050 * {@code
051 * xmlns:m.addins='urn:import:gwt.material.design.addins.client.ui'
052 * }
053 * </pre>
054 * <p>
055 * <h3>UiBinder Usage:</h3>
056 * <pre>
057 * {
058 * @code
059 * <m.addins:MaterialMasonry>
060 *     <m:MaterialColumn grid="l1" padding="4" backgroundColor="blue" height="200px">
061 *         <m:MaterialLabel text="1"/>
062 *     </m:MaterialColumn>
063 *     &lt;-- Other columns here -->
064 * </m.addins:MaterialMasonry>
065 * }
066 * </pre>
067 *
068 * @author kevzlou7979
069 * @see <a href="http://gwtmaterialdesign.github.io/gwt-material-demo/#masonry">Material Masonry</a>
070 * @see <a href="https://github.com/desandro/masonry">Masonry 4.0.0</a>
071 */
072//@formatter:on
073public class MaterialMasonry extends MaterialRow implements JsLoader, HasDurationTransition, HasMasonryHandler {
074
075    static {
076        if (MaterialAddins.isDebug()) {
077            MaterialDesignBase.injectDebugJs(MaterialMasonryDebugClientBundle.INSTANCE.masonryJsDebug());
078            MaterialDesignBase.injectDebugJs(MaterialMasonryDebugClientBundle.INSTANCE.imageLoadedJsDebug());
079        } else {
080            MaterialDesignBase.injectJs(MaterialMasonryClientBundle.INSTANCE.masonryJs());
081            MaterialDesignBase.injectJs(MaterialMasonryClientBundle.INSTANCE.imageLoadedJs());
082        }
083    }
084
085    private MaterialWidget sizerDiv = new MaterialWidget(Document.get().createDivElement());
086    private JsMasonryOptions options = JsMasonryOptions.create();
087    private JsMasonry masonryElement;
088    private Widget target;
089
090    public MaterialMasonry() {
091        super(Document.get().createDivElement(), AddinsCssName.MASONRY, CssName.ROW);
092
093        sizerDiv.setWidth("8.3333%");
094        sizerDiv.setStyleName(AddinsCssName.COL_SIZER);
095        add(sizerDiv);
096    }
097
098    @Override
099    protected void onLoad() {
100        super.onLoad();
101
102        load();
103
104        masonryElement.on(MasonryEvents.REMOVE_COMPLETE, (e, param1) -> {
105            RemoveCompleteEvent.fire(this, target);
106            return true;
107        });
108
109        masonryElement.on(MasonryEvents.LAYOUT_COMPLETE, (e, param1) -> {
110            LayoutCompleteEvent.fire(this);
111            return true;
112        });
113    }
114
115    @Override
116    public void load() {
117        masonryElement = $(getElement());
118        masonryElement.imagesLoaded(() -> masonryElement.masonry(options));
119    }
120
121    @Override
122    protected void onUnload() {
123        super.onUnload();
124
125        unload();
126    }
127
128    @Override
129    public void unload() {
130        if (masonryElement != null) {
131            masonryElement.masonry("destroy");
132            masonryElement.off(MasonryEvents.REMOVE_COMPLETE);
133            masonryElement.off(MasonryEvents.LAYOUT_COMPLETE);
134        }
135    }
136
137    @Override
138    public void reload() {
139        // reload items and layout specific to masonry plugin
140        if (masonryElement != null) {
141            layout();
142            reloadItems();
143        }
144    }
145
146    @Override
147    public boolean remove(Widget w) {
148        return remove((IsWidget) w);
149    }
150
151    @Override
152    public boolean remove(int index) {
153        remove(getWidget(index));
154        return true;
155    }
156
157    @Override
158    public boolean remove(IsWidget child) {
159        Widget widget = (Widget) child;
160        masonryRemove(widget);
161        reload();
162        return true;
163    }
164
165    /**
166     * Remove the item with Masonry support
167     */
168    protected void masonryRemove(Widget target) {
169        this.target = target;
170        $(getElement()).masonry(options).masonry("remove", target.getElement());
171    }
172
173    @Override
174    public void clear() {
175        for (Widget widget : getChildren()) {
176            remove(widget);
177        }
178    }
179
180    @Override
181    public void add(Widget child) {
182        super.add(child);
183        reload();
184    }
185
186    @Override
187    protected void add(Widget child, com.google.gwt.user.client.Element container) {
188        super.add(child, container);
189        reload();
190    }
191
192    @Override
193    protected void insert(Widget child, com.google.gwt.user.client.Element container, int beforeIndex, boolean domInsert) {
194        super.insert(child, container, beforeIndex, domInsert);
195        reload();
196    }
197
198    @Override
199    public void insert(Widget child, int beforeIndex) {
200        super.insert(child, beforeIndex);
201        reload();
202    }
203
204    /**
205     * Reload all items inside the masonry
206     */
207    protected void reloadItems() {
208        masonryElement.masonry(options).masonry("reloadItems");
209    }
210
211    /**
212     * Layout remaining item elements
213     */
214    protected void layout() {
215        masonryElement.masonry(options).masonry("layout");
216    }
217
218    /**
219     * Get the item selector.
220     */
221    public String getItemSelector() {
222        return options.itemSelector;
223    }
224
225    /**
226     * Specifies which child elements will be used as item elements in the
227     * layout.It's .col by default for grid components.
228     */
229    public void setItemSelector(String itemSelector) {
230        options.itemSelector = itemSelector;
231    }
232
233    /**
234     * Get the percent position boolean value.
235     */
236    public boolean isPercentPosition() {
237        return options.percentPosition;
238    }
239
240    /**
241     * Sets item positions in percent values, rather than pixel values. percentPosition: true works
242     * well with percent-width items, as items will not transition their position on resize.
243     */
244    public void setPercentPosition(boolean percentPosition) {
245        options.percentPosition = percentPosition;
246    }
247
248    /**
249     * Get the boolean value of origin left.
250     */
251    public boolean isOriginLeft() {
252        return options.originLeft;
253    }
254
255    /**
256     * Controls the horizontal flow of the layout. By default, item elements start positioning at the
257     * left, with originLeft: true. Set originLeft: false for right-to-left layouts.
258     */
259    public void setOriginLeft(boolean originLeft) {
260        options.originLeft = originLeft;
261    }
262
263    /**
264     * Get the boolean value of origin top.
265     */
266    public boolean isOriginTop() {
267        return options.originTop;
268    }
269
270    /**
271     * Controls the vertical flow of the layout. By default, item elements start positioning at the top,
272     * with originTop: true. Set originTop: false for bottom-up layouts. It’s like Tetris
273     */
274    public void setOriginTop(boolean originTop) {
275        options.originTop = originTop;
276    }
277
278    public MaterialWidget getSizerDiv() {
279        return sizerDiv;
280    }
281
282    @Override
283    public void setDuration(int duration) {
284        options.transitionDuration = duration + "ms";
285    }
286
287    @Override
288    public int getDuration() {
289        return options.transitionDuration != null ? Integer.parseInt(options.transitionDuration.replace("ms", "")) : 0;
290    }
291
292    @Override
293    public HandlerRegistration addLayoutCompleteHandler(LayoutCompleteEvent.LayoutCompleteHandler handler) {
294        return addHandler(handler, LayoutCompleteEvent.TYPE);
295    }
296
297    @Override
298    public HandlerRegistration addRemoveCompleteHandler(RemoveCompleteEvent.RemoveCompleteHandler handler) {
299        return addHandler(handler, RemoveCompleteEvent.TYPE);
300    }
301}