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.client.ui.table;
021
022import com.google.gwt.dom.client.Style;
023import com.google.gwt.event.logical.shared.AttachEvent;
024import com.google.gwt.event.shared.HandlerRegistration;
025import com.google.gwt.user.client.ui.Panel;
026import gwt.material.design.client.base.constants.TableCssName;
027import gwt.material.design.client.data.events.InsertColumnEvent;
028import gwt.material.design.client.data.events.InsertColumnHandler;
029import gwt.material.design.client.data.events.RemoveColumnEvent;
030import gwt.material.design.client.data.events.RemoveColumnHandler;
031import gwt.material.design.client.data.events.SetupHandler;
032import gwt.material.design.client.ui.table.events.StretchEvent;
033import gwt.material.design.client.ui.table.events.StretchHandler;
034import gwt.material.design.jquery.client.api.JQueryElement;
035import gwt.material.design.client.constants.Alignment;
036import gwt.material.design.client.constants.HideOn;
037import gwt.material.design.client.constants.IconType;
038import gwt.material.design.client.constants.WavesType;
039import gwt.material.design.client.data.DataView;
040import gwt.material.design.client.js.Js;
041import gwt.material.design.client.js.JsTableElement;
042import gwt.material.design.client.ui.MaterialCheckBox;
043import gwt.material.design.client.ui.MaterialDropDown;
044import gwt.material.design.client.ui.MaterialIcon;
045import gwt.material.design.client.ui.html.ListItem;
046import gwt.material.design.client.ui.html.Span;
047import gwt.material.design.client.ui.table.cell.Column;
048
049import static gwt.material.design.jquery.client.api.JQuery.$;
050
051/**
052 * The standard Material data table with custom "outer" components such as,
053 * table icon, table title, stretch functionality, column toggling, etc.
054 *
055 * @author Ben Dol
056 *
057 * @see <a href="http://gwtmaterialdesign.github.io/gwt-material-demo/#datatable">Material Data Table</a>
058 * @see <a href="https://material.io/guidelines/components/data-tables.html">Material Design Specification</a>
059 */
060public class MaterialDataTable<T> extends AbstractDataTable<T> implements InsertColumnHandler<T>, RemoveColumnHandler {
061
062    private JQueryElement stretchContainer;
063
064    // Interface
065    private MaterialIcon tableIcon;
066    private Span tableTitle;
067    private MaterialIcon stretchIcon;
068    private MaterialIcon columnMenuIcon;
069    private MaterialDropDown menu;
070
071    public MaterialDataTable() {
072        loadInternalEvents();
073    }
074
075    public MaterialDataTable(DataView<T> dataView) {
076        super(dataView);
077        loadInternalEvents();
078    }
079
080    public MaterialDataTable(TableScaffolding scaffolding) {
081        super(scaffolding);
082        loadInternalEvents();
083    }
084
085    public MaterialDataTable(DataView<T> dataView, TableScaffolding scaffolding) {
086        super(dataView, scaffolding);
087        loadInternalEvents();
088    }
089
090    @Override
091    protected void onLoad() {
092        super.onLoad();
093
094        // Attempt to rebuild encase the widgets have been unloaded.
095        // In some use cases (like GWTP) the child widgets aren't unloaded.
096        // So we make sure we check the state of each Widget.
097        build();
098    }
099
100    @Override
101    protected void build() {
102        super.build();
103
104        Panel infoPanel = scaffolding.getInfoPanel();
105        Panel toolPanel = scaffolding.getToolPanel();
106
107        if(tableIcon == null || !tableIcon.isAttached()) {
108            // table icon
109            tableIcon = new MaterialIcon(IconType.VIEW_LIST);
110            infoPanel.add(tableIcon);
111        }
112
113        if(tableTitle == null || !tableTitle.isAttached()) {
114            // table title
115            tableTitle = new Span("Table Title");
116            tableTitle.addStyleName(TableCssName.TITLE);
117            infoPanel.add(tableTitle);
118        }
119
120        if(stretchIcon == null || !stretchIcon.isAttached()) {
121            // stretch icon
122            stretchIcon = new MaterialIcon(IconType.FULLSCREEN);
123            stretchIcon.setWaves(WavesType.LIGHT);
124            stretchIcon.setCircle(true);
125            stretchIcon.setId("stretch");
126            stretchIcon.getElement().getStyle().setCursor(Style.Cursor.POINTER);
127            toolPanel.add(stretchIcon);
128        }
129
130        if(columnMenuIcon == null || !columnMenuIcon.isAttached()) {
131            // menu icon
132            columnMenuIcon = new MaterialIcon(IconType.MORE_VERT);
133            columnMenuIcon.setHideOn(HideOn.HIDE_ON_SMALL_DOWN);
134            columnMenuIcon.setWaves(WavesType.LIGHT);
135            columnMenuIcon.setCircle(true);
136            columnMenuIcon.setId("columnToggle");
137            columnMenuIcon.getElement().getStyle().setCursor(Style.Cursor.POINTER);
138            toolPanel.add(columnMenuIcon);
139        }
140
141        if(stretchContainer == null) {
142            // stretch container
143            stretchContainer = $("body");
144        }
145
146        setupToolPanel();
147        setupMenu();
148    }
149
150    protected void setupToolPanel() {
151        // Stretch click handler
152        $(scaffolding.getToolPanel()).find("i#stretch").off("click").on("click", e -> {
153            stretch();
154
155            e.preventDefault();
156            return true;
157        });
158    }
159
160    protected void setupMenu() {
161        // Setup menu checkboxes
162        // This will allow the user to toggle columns
163
164        if(menu == null) {
165            // dropdown structure
166            menu = new MaterialDropDown(columnMenuIcon);
167            scaffolding.getToolPanel().add(menu);
168
169            // Menu initialization
170            menu.setInDuration(300);
171            menu.setOutDuration(225);
172            menu.setConstrainWidth(false);
173            menu.setHover(false);
174            menu.setGutter(0);
175            menu.setBelowOrigin(false);
176            menu.setAlignment(Alignment.LEFT);
177            menu.setHideOn(HideOn.HIDE_ON_SMALL_DOWN);
178            menu.getElement().getStyle().setProperty("minWidth", "200px");
179        }
180
181        JQueryElement $menu = $(menu);
182        $menu.find("li label").off("tap click");
183        $menu.find("li label").on("tap click", e -> {
184            JQueryElement $this = $(e.getCurrentTarget());
185
186            String forBox = ((String) $this.attr("for")).replace(getView().getId() + "-", "");
187            if(Js.isTrue(forBox)) {
188                JQueryElement thd = $("th#" + forBox + ",td#" + forBox, this);
189                boolean checked = $this.prev().is(":checked");
190
191                thd.each((index, el) -> {
192                    JQueryElement cell = $(el);
193                    if(checked) {
194                        cell.hide();
195                    } else {
196                        cell.show();
197                    }
198                });
199
200                // Update the sticky table header widths
201                scaffolding.getTable().getJsElement().stickyTableHeaders("updateWidth");
202
203                // Recalculate the subheader
204                getView().getSubheaderLib().recalculate(true);
205            }
206            return true;
207        });
208
209        // Stop each menu item from closing the dropdown
210        $menu.find("li").off("touchstart click");
211        $menu.find("li").on("touchstart click", e -> {
212            e.stopPropagation();
213            return true;
214        });
215    }
216
217    @Override
218    public void onInsertColumn(InsertColumnEvent<T> event) {
219        int beforeIndex = event.getBeforeIndex();
220        Column<T, ?> column = event.getColumn();
221        String header = event.getHeader();
222
223        SetupHandler handler = e -> {
224            int index = beforeIndex + getView().getColumnOffset();
225            String ref = getView().getId() + "-col" + index;
226
227            MaterialCheckBox toggleBox = new MaterialCheckBox(new ListItem().getElement());
228            JQueryElement input = $(toggleBox).find("input");
229            input.attr("id", ref);
230
231            JQueryElement label = $(toggleBox).find("label");
232            label.text(column.getName());
233            label.attr("for", ref);
234
235            toggleBox.setValue(true);
236            menu.add(toggleBox);
237
238            // We will hide the empty header menu items
239            if (header.isEmpty()) {
240                toggleBox.setVisible(false);
241            }
242
243            setupMenu();
244            reindexToggles();
245        };
246
247        if(getView().isSetup()) {
248            handler.onSetup(null);
249        } else {
250            addSetupHandler(handler);
251        }
252    }
253
254    @Override
255    public void onRemoveColumn(RemoveColumnEvent event) {
256        SetupHandler handler = e -> {
257            int index = event.getIndex() + getView().getColumnOffset();
258            $(menu).find("li input#col" + index).parent().remove();
259            reindexToggles();
260        };
261
262        if(getView().isSetup()) {
263            handler.onSetup(null);
264        } else {
265            addSetupHandler(handler);
266        }
267    }
268
269    private void reindexToggles() {
270        int colOffset = getView().getColumnOffset();
271        $("li", menu).each((index, e) -> {
272            String ref = getView().getId() + "-col" + ((Double)index + colOffset);
273
274            JQueryElement input = $(e).find("input");
275            input.attr("id", ref);
276
277            JQueryElement label = $(e).find("label");
278            label.attr("for", ref);
279        });
280    }
281
282    public void stretch() {
283        stretch(true);
284    }
285
286    public void stretch(boolean fireEvent) {
287        $this().toggleClass(TableCssName.STRETCH);
288
289        // Make sure the body doesn't display scrollbar
290        body().toggleClass(TableCssName.OVERFLOW_HIDDEN);
291
292        // Update table header widths
293        JsTableElement tableJs = scaffolding.getTable().getJsElement();
294        tableJs.stickyTableHeaders("updateWidth");
295        tableJs.stickyTableHeaders("toggleHeaders");
296
297        // Recalculate subheaders
298        getView().getSubheaderLib().recalculate(true);
299
300        if(fireEvent) {
301            // Fire table stretch event
302            StretchEvent.fire(this, $this().hasClass(TableCssName.STRETCH));
303        }
304    }
305
306    public MaterialIcon getStretchIcon() {
307        return stretchIcon;
308    }
309
310    public MaterialIcon getColumnMenuIcon() {
311        return columnMenuIcon;
312    }
313
314    public MaterialIcon getTableIcon() {
315        return tableIcon;
316    }
317
318    public MaterialDropDown getMenu() {
319        return menu;
320    }
321
322    public Span getTableTitle() {
323        return tableTitle;
324    }
325
326    /**
327     * Add a handler that is triggered when the table is stretched.
328     */
329    public HandlerRegistration addStretchHandler(StretchHandler handler) {
330        return addHandler(handler, StretchEvent.TYPE);
331    }
332
333    /**
334     * Load events
335     */
336    protected void loadInternalEvents() {
337        // Register data view events, these are removed onUnload.
338        addInsertColumnHandler(this);
339        addRemoveColumnHandler(this);
340    }
341}