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.stepper;
021
022import com.google.gwt.core.client.GWT;
023import com.google.gwt.dom.client.Document;
024import com.google.gwt.event.logical.shared.SelectionEvent;
025import com.google.gwt.event.logical.shared.SelectionHandler;
026import com.google.gwt.event.shared.HandlerRegistration;
027import com.google.gwt.user.client.ui.Widget;
028import com.google.gwt.view.client.SelectionChangeEvent;
029import com.google.gwt.view.client.SelectionChangeEvent.Handler;
030import com.google.gwt.view.client.SelectionChangeEvent.HasSelectionChangedHandlers;
031import gwt.material.design.addins.client.MaterialAddins;
032import gwt.material.design.addins.client.base.constants.AddinsCssName;
033import gwt.material.design.addins.client.stepper.base.HasStepsHandler;
034import gwt.material.design.addins.client.stepper.constants.State;
035import gwt.material.design.addins.client.stepper.events.CompleteEvent;
036import gwt.material.design.addins.client.stepper.events.NextEvent;
037import gwt.material.design.addins.client.stepper.events.PreviousEvent;
038import gwt.material.design.addins.client.stepper.events.StartEvent;
039import gwt.material.design.client.MaterialDesignBase;
040import gwt.material.design.client.base.HasAxis;
041import gwt.material.design.client.base.HasError;
042import gwt.material.design.client.base.MaterialWidget;
043import gwt.material.design.client.base.mixin.CssNameMixin;
044import gwt.material.design.client.constants.Axis;
045import gwt.material.design.client.js.Window;
046import gwt.material.design.client.ui.MaterialLoader;
047import gwt.material.design.client.ui.animate.MaterialAnimation;
048import gwt.material.design.client.ui.animate.Transition;
049import gwt.material.design.client.ui.html.Div;
050import gwt.material.design.client.ui.html.Span;
051
052//@formatter:off
053
054/**
055 * Steppers convey progress through numbered steps. They may also be used for navigation.
056 * <p>
057 * <h3>XML Namespace Declaration</h3>
058 * <pre>
059 * {@code
060 * xmlns:ma='urn:import:gwt.material.design.addins.client'
061 * }
062 * </pre>
063 * <p>
064 * <h3>UiBinder Usage:</h3>
065 * <pre>
066 * {@code
067 *  <ma:stepper.MaterialStepper ui:field="stepper">
068 *      <ma:stepper.MaterialStep step="1" title="Name of Step 1">
069 *          <m:MaterialPanel width="100%" height="300px" backgroundColor="grey lighten-2"/>
070 *          <m:MaterialButton ui:field="btnContinue1" text="Continue to Step 2" grid="l4" marginTop="12" backgroundColor="blue" textColor="white" waves="DEFAULT"/>
071 *          <m:MaterialButton ui:field="btnPrev1" text="Cancel" grid="l4" marginTop="12" type="FLAT" waves="DEFAULT"/>
072 *      </ma:stepper.MaterialStep>
073 *      &lt;!-- Other Step components here -->
074 *  </ma:stepper.MaterialStepper>
075 * }
076 * </pre>
077 *
078 * @author kevzlou7979
079 * @see <a href="http://gwtmaterialdesign.github.io/gwt-material-demo/#steppers">Material Steppers</a>
080 * @see <a href="https://material.io/guidelines/components/steppers.html">Material Design Specification</a>
081 */
082// @formatter:on
083public class MaterialStepper extends MaterialWidget implements HasAxis, HasError, SelectionHandler<MaterialStep>,
084        HasSelectionChangedHandlers, HasStepsHandler {
085
086    static {
087        if (MaterialAddins.isDebug()) {
088            MaterialDesignBase.injectCss(MaterialStepperDebugClientBundle.INSTANCE.stepperDebugCss());
089        } else {
090            MaterialDesignBase.injectCss(MaterialStepperClientBundle.INSTANCE.stepperCss());
091        }
092    }
093
094    private int currentStepIndex = 0;
095    private boolean stepSkippingAllowed = true;
096    private boolean detectOrientation = false;
097    private Div divFeedback = new Div();
098    private Span feedbackSpan = new Span();
099    private HandlerRegistration orientationHandler;
100
101    private CssNameMixin<MaterialStepper, Axis> axisMixin;
102
103    public MaterialStepper() {
104        super(Document.get().createDivElement(), AddinsCssName.STEPPER);
105
106        divFeedback.setStyleName(AddinsCssName.FEEDBACK);
107        divFeedback.add(feedbackSpan);
108    }
109
110    @Override
111    protected void onLoad() {
112        super.onLoad();
113
114        if (getChildren().size() != 0) {
115            StartEvent.fire(MaterialStepper.this);
116            goToStep(currentStepIndex + 1);
117        }
118
119        setDetectOrientation(detectOrientation);
120    }
121
122    public void setDetectOrientation(boolean detectOrientation) {
123        this.detectOrientation = detectOrientation;
124
125        if (orientationHandler != null) {
126            orientationHandler.removeHandler();
127            orientationHandler = null;
128        }
129
130        if (detectOrientation) {
131            orientationHandler = registerHandler(Window.addResizeHandler(resizeEvent -> detectAndApplyOrientation()));
132            detectAndApplyOrientation();
133        }
134    }
135
136    protected void detectAndApplyOrientation() {
137        if (Window.matchMedia("(orientation: portrait)")) {
138            setAxis(Axis.VERTICAL);
139        } else {
140            setAxis(Axis.HORIZONTAL);
141        }
142    }
143
144    public boolean isDetectOrientation() {
145        return detectOrientation;
146    }
147
148    /**
149     * Specific method to add {@link MaterialStep}s to the stepper.
150     */
151    public void add(MaterialStep step) {
152        this.add((Widget) step);
153        step.setAxis(getAxis());
154        registerHandler(step.addSelectionHandler(this));
155    }
156
157    /**
158     * Go to next step, used by linear stepper.
159     */
160    public void nextStep() {
161        if (currentStepIndex >= getWidgetCount() - 1) {
162            CompleteEvent.fire(MaterialStepper.this, currentStepIndex + 1);
163        } else {
164            Widget w = getWidget(currentStepIndex);
165            if (w instanceof MaterialStep) {
166                MaterialStep step = (MaterialStep) w;
167                step.setActive(false);
168
169                step.setSuccess(step.getDescription());
170
171                // next step
172                int nextStepIndex = getWidgetIndex(step) + 1;
173                if (nextStepIndex >= 0) {
174                    for (int i = nextStepIndex; i < getWidgetCount(); i++) {
175                        w = getWidget(i);
176                        if (!(w instanceof MaterialStep)) {
177                            continue;
178                        }
179                        MaterialStep nextStep = (MaterialStep) w;
180                        if (nextStep.isEnabled() && nextStep.isVisible()) {
181                            nextStep.setActive(true);
182                            setCurrentStepIndex(i);
183                            NextEvent.fire(MaterialStepper.this);
184                            break;
185                        }
186                    }
187                }
188            }
189        }
190    }
191
192    /**
193     * Go to previous step , used by linear stepper.
194     */
195    public void prevStep() {
196        if (currentStepIndex > 0) {
197            Widget w = getWidget(currentStepIndex);
198            if (w instanceof MaterialStep) {
199                MaterialStep step = (MaterialStep) w;
200                step.setActive(false);
201
202                // prev step
203                int prevStepIndex = getWidgetIndex(step) - 1;
204                if (prevStepIndex >= 0) {
205                    for (int i = prevStepIndex; i >= 0; i--) {
206                        w = getWidget(i);
207                        if (!(w instanceof MaterialStep)) {
208                            continue;
209                        }
210                        MaterialStep prevStep = (MaterialStep) w;
211                        if (prevStep.isEnabled() && prevStep.isVisible()) {
212                            prevStep.setActive(true);
213                            setCurrentStepIndex(i);
214                            PreviousEvent.fire(MaterialStepper.this);
215                            break;
216                        }
217                    }
218                }
219            }
220        } else {
221            GWT.log("You have reached the minimum step.");
222        }
223    }
224
225    /**
226     * Go to specific step manually by setting which step index you want to go.
227     */
228    public void goToStep(int step) {
229        for (int i = 0; i < getWidgetCount(); i++) {
230            Widget w = getWidget(i);
231            if (w instanceof MaterialStep) {
232                ((MaterialStep) w).setActive(false);
233            }
234        }
235
236        Widget w = getWidget(step - 1);
237        if (w instanceof MaterialStep) {
238            ((MaterialStep) w).setActive(true);
239        }
240        setCurrentStepIndex(step - 1);
241    }
242
243    /**
244     * Go to the specfic {@link MaterialStep}.
245     */
246    public void goToStep(MaterialStep step) {
247        for (int i = 0; i < getWidgetCount(); i++) {
248            Widget w = getWidget(i);
249            if (w instanceof MaterialStep) {
250                MaterialStep materialStep = (MaterialStep) w;
251                boolean active = materialStep.equals(step);
252                materialStep.setActive(active);
253                if (active) {
254                    setCurrentStepIndex(i);
255                }
256            }
257        }
258    }
259
260    /**
261     * Go to the step with the specified step id.
262     *
263     * @see MaterialStep#getStep()
264     */
265    public void goToStepId(int id) {
266        for (int i = 0; i < getWidgetCount(); i++) {
267            Widget w = getWidget(i);
268            if (w instanceof MaterialStep) {
269                MaterialStep materialStep = (MaterialStep) w;
270                boolean active = materialStep.getStep() == id;
271                materialStep.setActive(active);
272                if (active) {
273                    setCurrentStepIndex(i);
274                }
275            }
276        }
277    }
278
279    /**
280     * Reset the Stepper to initial step (first step).
281     */
282    public void reset() {
283        goToStep(1);
284        clearErrorOrSuccess();
285    }
286
287    /**
288     * Called internally when the index is changed. Fires a {@link SelectionChangeEvent}
289     * when the current index changes.
290     */
291    protected void setCurrentStepIndex(int currentStepIndex) {
292        if (this.currentStepIndex != currentStepIndex) {
293            this.currentStepIndex = currentStepIndex;
294            SelectionChangeEvent.fire(this);
295        }
296
297    }
298
299    public int getCurrentStepIndex() {
300        return currentStepIndex;
301    }
302
303    @Override
304    public void setAxis(Axis axis) {
305        getAxisMixin().setCssName(axis);
306        for (int i = 0; i < getWidgetCount(); i++) {
307            Widget w = getWidget(i);
308            if (w instanceof MaterialStep) {
309                ((MaterialStep) w).setAxis(axis);
310            }
311        }
312    }
313
314    @Override
315    public Axis getAxis() {
316        return getAxisMixin().getCssName();
317    }
318
319    /**
320     * Gets the current step component.
321     */
322    public MaterialStep getCurrentStep() {
323        if (currentStepIndex > getWidgetCount() - 1 || currentStepIndex < 0) {
324            return null;
325        }
326        Widget w = getWidget(currentStepIndex);
327        if (w instanceof MaterialStep) {
328            return (MaterialStep) w;
329        }
330        return null;
331    }
332
333    @Override
334    public void setError(String error) {
335        getCurrentStep().setError(error);
336    }
337
338    @Override
339    public void setSuccess(String success) {
340        getCurrentStep().setSuccess(success);
341    }
342
343    @Override
344    public void setHelperText(String helperText) {
345        getCurrentStep().setDescription(helperText);
346    }
347
348    @Override
349    public void clearErrorOrSuccess() {
350        for (int i = 0; i < getWidgetCount(); i++) {
351            Widget w = getWidget(i);
352            if (w instanceof MaterialStep) {
353                ((MaterialStep) w).clearErrorOrSuccess();
354            }
355        }
356    }
357
358    /**
359     * Get feedback message.
360     */
361    public String getFeedback() {
362        return feedbackSpan.getElement().getInnerHTML();
363    }
364
365    /**
366     * Show feedback message and circular loader on body container
367     */
368    public void showFeedback(String feedbackText) {
369        feedbackSpan.setText(feedbackText);
370        new MaterialAnimation().transition(Transition.FADEINUP).duration(400).animate(feedbackSpan);
371        MaterialLoader.loading(true, getCurrentStep().getDivBody());
372        add(divFeedback);
373    }
374
375    /**
376     * Hide feedback message and circular loader on body container.
377     */
378    public void hideFeedback() {
379        divFeedback.removeFromParent();
380    }
381
382    /**
383     * Sets whether the user is allowed to skip steps by clicking on the step title.
384     * The default is <code>true</code>.
385     */
386    public void setStepSkippingAllowed(boolean stepSkippingAllowed) {
387        this.stepSkippingAllowed = stepSkippingAllowed;
388    }
389
390    /**
391     * Returns whether the user is allowed to skip steps by clicking on the step title.
392     * The default is <code>true</code>.
393     */
394    public boolean isStepSkippingAllowed() {
395        return stepSkippingAllowed;
396    }
397
398    public Span getFeedbackSpan() {
399        return feedbackSpan;
400    }
401
402    /**
403     * Called when a step title is clicked.
404     */
405    @Override
406    public void onSelection(SelectionEvent<MaterialStep> event) {
407        if (stepSkippingAllowed) {
408            if (event.getSelectedItem().getState() == State.SUCCESS) {
409                goToStep(event.getSelectedItem());
410            }
411        }
412    }
413
414    @Override
415    public HandlerRegistration addSelectionChangeHandler(final Handler handler) {
416        return addHandler(handler, SelectionChangeEvent.getType());
417    }
418
419    @Override
420    public HandlerRegistration addStartHandler(StartEvent.StartHandler handler) {
421        return addHandler(handler, StartEvent.TYPE);
422    }
423
424    @Override
425    public HandlerRegistration addCompleteHandler(CompleteEvent.CompleteHandler handler) {
426        return addHandler(handler, CompleteEvent.TYPE);
427    }
428
429    @Override
430    public HandlerRegistration addNextHandler(NextEvent.NextHandler handler) {
431        return addHandler(handler, NextEvent.TYPE);
432    }
433
434    @Override
435    public HandlerRegistration addPreviousHandler(PreviousEvent.PreviousHandler handler) {
436        return addHandler(handler, PreviousEvent.TYPE);
437    }
438
439    protected CssNameMixin<MaterialStepper, Axis> getAxisMixin() {
440        if (axisMixin == null) {
441            axisMixin = new CssNameMixin<>(this);
442        }
443        return axisMixin;
444    }
445}