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 * <!-- 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}