View Javadoc
1   /**
2    * Copyright 2005-2015 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.uif.element;
17  
18  import org.apache.commons.collections.CollectionUtils;
19  import org.apache.commons.lang3.StringUtils;
20  import org.kuali.rice.krad.datadictionary.parse.BeanTag;
21  import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
22  import org.kuali.rice.krad.messages.MessageService;
23  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
24  import org.kuali.rice.krad.uif.CssConstants;
25  import org.kuali.rice.krad.uif.UifConstants;
26  import org.kuali.rice.krad.uif.util.LifecycleElement;
27  
28  import java.util.ArrayList;
29  import java.util.Collection;
30  import java.util.Iterator;
31  import java.util.LinkedHashMap;
32  import java.util.List;
33  import java.util.Map;
34  
35  /**
36   * Element which shows a visual progress bar alongside a set of steps, to be used by wizard or multi-step
37   * processes, which reflects the current progress based on value of currentStep.
38   *
39   * @author Kuali Rice Team (rice.collab@kuali.org)
40   */
41  @BeanTag(name = "stepProgressBar-bean", parent = "Uif-StepProgressBar")
42  public class StepProgressBar extends ProgressBar {
43      private static final long serialVersionUID = 1053164737424481519L;
44  
45      private Map<String, String> steps;
46      private List<String> stepLabelClasses;
47      private List<String> accessibilityText;
48  
49      private String currentStep;
50      private String completeStep;
51  
52      private Integer verticalHeight;
53      private Integer verticalStepHeight;
54  
55      public StepProgressBar() {
56          steps = new LinkedHashMap<String, String>();
57          accessibilityText = new ArrayList<String>();
58          stepLabelClasses = new ArrayList<String>();
59      }
60  
61      /**
62       * Populates the stepLabelClasses, accessibilityText, segmentSizes, and segmentClasses lists based on the settings
63       * of this StepProgressBar.
64       *
65       * {@inheritDoc}
66       */
67      @Override
68      public void performFinalize(Object model, LifecycleElement parent) {
69          // If a percentageComplete value is set, use it to try to determine the current step, otherwise if currentStep
70          // is set just use that (null percentComplete value)
71          if (this.getPercentComplete() != null && currentStep == null) {
72              calculateCurrentStepFromPercentage();
73          } else if (currentStep != null) {
74              this.setPercentComplete(null);
75          }
76  
77          super.performFinalize(model, parent);
78  
79          MessageService messageService = KRADServiceLocatorWeb.getMessageService();
80  
81          // Initializing and checking for validity of values:
82          String cssDimension = CssConstants.WIDTH;
83          if (this.isVertical()) {
84              cssDimension = CssConstants.HEIGHT;
85              this.addStyleClass(CssConstants.ProgressBar.VERTICAL_STEP_PROGRESS_BAR);
86          }
87  
88          int totalSteps = steps.size();
89          if (totalSteps == 0) {
90              throw new RuntimeException(
91                      "At least one step is required for a StepProgressBar: " + this.getId() + " with parent: " + parent
92                              .getId());
93          }
94  
95          boolean explicitlySetPercentages = CollectionUtils.isNotEmpty(getSegmentPercentages());
96          boolean explicitlySetClasses = CollectionUtils.isNotEmpty(this.getSegmentClasses());
97          if (explicitlySetPercentages && explicitlySetClasses && this.getSegmentClasses().size() != this
98                  .getSegmentPercentages().size()) {
99              throw new RuntimeException(
100                     "If segmentPercentages are set on a StepProgressBar type, and segmentClasses are also "
101                             + "set, the lists MUST contain the same number of items");
102         }
103 
104         // Populate the information used by the template based on settings of this StepProgressBar
105         populateProgressBarRenderingLists(totalSteps, cssDimension, explicitlySetPercentages, explicitlySetClasses);
106 
107         // Explicitly set the vertical height for vertical cases where the verticalHeight is not set using
108         // verticalStepHeight
109         if (this.isVertical() && getVerticalHeight() == null) {
110             setVerticalHeight(getSegmentSizes().size() * verticalStepHeight);
111         }
112 
113         // If the step is considered complete, set the aria attributes appropriately
114         if (currentStep != null && currentStep.equals(completeStep)) {
115             this.addAriaAttribute(UifConstants.AriaAttributes.VALUE_NOW, Integer.toString(steps.size()));
116             this.addAriaAttribute(UifConstants.AriaAttributes.VALUE_TEXT, messageService.getMessageText(
117                     "accessibility.progressBar.complete"));
118         }
119 
120         // Add aria attributes
121         this.addAriaAttribute(UifConstants.AriaAttributes.VALUE_MIN, "0");
122         this.addAriaAttribute(UifConstants.AriaAttributes.VALUE_MAX, Integer.toString(totalSteps));
123     }
124 
125     /**
126      * Calculate the current step based on a percentage value.
127      *
128      * @return the current step key which is at that percentage of total steps
129      */
130     private String calculateCurrentStepFromPercentage() {
131         if (getPercentComplete() == 0) {
132             return "";
133         } else if (getPercentComplete() == 100) {
134             return completeStep;
135         }
136 
137         int size = steps.size();
138         double currentStep = Math.ceil(size * this.getPercentComplete());
139 
140         String key = "";
141         Iterator<String> stepIterator = steps.keySet().iterator();
142         for (int step = 0; stepIterator.hasNext() && step <= currentStep; step++) {
143             key = stepIterator.next();
144         }
145 
146         this.setPercentComplete(null);
147 
148         return key;
149     }
150 
151     /**
152      * Populate the information used by the template based on settings of this StepProgressBar by iterating of
153      * the steps and setting classes and other rendering info in list to be used by the template.
154      *
155      * @param totalSteps the total steps in this StepProgressBar
156      * @param cssDimension the css dimension property to use for bar sizes
157      * @param explicitlySetPercentages true if bar percentages were manually set
158      * @param explicitlySetClasses true if bar classes wer manually set
159      */
160     public void populateProgressBarRenderingLists(int totalSteps, String cssDimension, boolean explicitlySetPercentages,
161             boolean explicitlySetClasses) {
162         MessageService messageService = KRADServiceLocatorWeb.getMessageService();
163 
164         double percentage = Math.floor(100 / totalSteps);
165         double percentTotal = 0;
166         boolean currentStepFound = false;
167 
168         // Bar is considered empty if currentStep is not set or is something that does not match a key
169         // so set currentStepFound to true to force the following loop to only create "empty" bars
170         if (StringUtils.isBlank(currentStep) || (!steps.containsKey(currentStep) && !currentStep.equals(
171                 completeStep))) {
172             currentStepFound = true;
173         }
174 
175         Iterator<String> stepIterator = steps.keySet().iterator();
176         for (int step = 0; stepIterator.hasNext() && step <= totalSteps; step++) {
177             String stepKey = stepIterator.next();
178 
179             double stepPercentage;
180 
181             // Retrieve/calculate the current stepPercentage and current percentageTotal of bars being processed
182             if (explicitlySetPercentages) {
183                 Integer percentageValue = getSegmentPercentages().get(step);
184                 stepPercentage = percentageValue;
185                 percentTotal += percentageValue;
186             } else {
187                 stepPercentage = percentage;
188                 percentTotal += percentage;
189             }
190 
191             // if there is some missing width to make 100% due to uneven division and we are on the final iteration,
192             // give the additional percentage to the last bar
193             if (!stepIterator.hasNext() && percentTotal < 100) {
194                 stepPercentage = (percentage + (100 - percentTotal));
195                 percentTotal = 100;
196             }
197 
198             String dimensionValue = stepPercentage + "%";
199 
200             // Default bar styles and screen reader text
201             String cssClasses =
202                     CssConstants.ProgressBar.PROGRESS_BAR + " " + CssConstants.ProgressBar.SUCCESS_PROGRESS_BAR;
203             String labelCssClasses = CssConstants.ProgressBar.STEP_LABEL + " " + CssConstants.ProgressBar.COMPLETE;
204             String srText = messageService.getMessageText("accessibility.progressBar.completeStep");
205 
206             // If current step, change styles and text appropriately.  When the step has already be found,
207             // the final bars are considered empty/incomplete steps
208             if (stepKey.equals(currentStep)) {
209                 currentStepFound = true;
210                 cssClasses = CssConstants.ProgressBar.PROGRESS_BAR + " " + CssConstants.ProgressBar.INFO_PROGRESS_BAR;
211                 labelCssClasses = CssConstants.ProgressBar.STEP_LABEL + " " + CssConstants.ProgressBar.ACTIVE;
212                 srText = messageService.getMessageText("accessibility.progressBar.currentStep");
213 
214                 // Set aria attributes for the current value
215                 this.addAriaAttribute(UifConstants.AriaAttributes.VALUE_NOW, Integer.toString(step));
216                 this.addAriaAttribute(UifConstants.AriaAttributes.VALUE_TEXT, srText + steps.get(stepKey));
217             } else if (currentStepFound) {
218                 cssClasses = CssConstants.ProgressBar.PROGRESS_BAR + " " + CssConstants.ProgressBar.EMPTY_PROGRESS_BAR;
219                 labelCssClasses = CssConstants.ProgressBar.STEP_LABEL;
220                 srText = messageService.getMessageText("accessibility.progressBar.futureStep");
221             }
222 
223             this.getSegmentSizes().add(cssDimension + dimensionValue);
224 
225             // Don't add default classes if custom classes have been set for the bars
226             if (!explicitlySetClasses) {
227                 this.getSegmentClasses().add(cssClasses);
228             }
229 
230             this.getStepLabelClasses().add(labelCssClasses);
231             this.accessibilityText.add(srText);
232         }
233     }
234 
235     /**
236      * The steps as key-value pairs for this StepProgressBar, where value is human-readable text.
237      *
238      * @return the map of steps for this StepProgressBar
239      */
240     @BeanTagAttribute(name = "steps", type = BeanTagAttribute.AttributeType.MAPVALUE)
241     public Map<String, String> getSteps() {
242         return steps;
243     }
244 
245     /**
246      * @see StepProgressBar#getSteps()
247      */
248     public void setSteps(Map<String, String> steps) {
249         this.steps = steps;
250     }
251 
252     /**
253      * The list of step values; framework only, not settable.
254      *
255      * @return the list of step values
256      */
257     public Collection<String> getStepCollection() {
258         return steps.values();
259     }
260 
261     /**
262      * The list of step label css classes in order of steps shown; framework only, not settable
263      *
264      * @return the list of step label css classes
265      */
266     public List<String> getStepLabelClasses() {
267         return stepLabelClasses;
268     }
269 
270     /**
271      * The list of additional screen reader only accessibility text to render per step, in order; framework only,
272      * not settable.
273      *
274      * @return the list of additional screen reader only accessibility text
275      */
276     public List<String> getAccessibilityText() {
277         return accessibilityText;
278     }
279 
280     /**
281      * The current step (by key) of this progress bar to be highlighted visually as the active step.
282      *
283      * @return the current step (by key)
284      */
285     @BeanTagAttribute(name = "currentStep")
286     public String getCurrentStep() {
287         return currentStep;
288     }
289 
290     /**
291      * @see StepProgressBar#getCurrentStep()
292      */
293     public void setCurrentStep(String currentStep) {
294         this.currentStep = currentStep;
295     }
296 
297     /**
298      * The key that when currentStep has this value, shows the step progress bar as fully complete; this key
299      * is must not be part of the steps being shown, by default this has a value of "SUCCESS".
300      *
301      * @return the completeStep key for showing this bar as fully complete
302      */
303     @BeanTagAttribute(name = "completeStep")
304     public String getCompleteStep() {
305         return completeStep;
306     }
307 
308     /**
309      * @see org.kuali.rice.krad.uif.element.StepProgressBar#getCompleteStep()
310      */
311     public void setCompleteStep(String completeStep) {
312         this.completeStep = completeStep;
313     }
314 
315     /**
316      * The height (in pixels) of the progress bar portion of this component, if this is not set, verticalStepHeight
317      * is used to calculate this value; only used when vertical property is true.
318      *
319      * @return the verticalHeight of the progress bar
320      */
321     @BeanTagAttribute(name = "verticalHeight")
322     public Integer getVerticalHeight() {
323         return verticalHeight;
324     }
325 
326     /**
327      * @see StepProgressBar#getVerticalHeight()
328      */
329     public void setVerticalHeight(Integer verticalHeight) {
330         this.verticalHeight = verticalHeight;
331     }
332 
333     /**
334      * The height (in pixels) allocated for each step for vertical step display used to calculate verticalHeight if not
335      * set, by default this is 75.
336      *
337      * @return the vertical step height used to calculate verticalHeight
338      */
339     @BeanTagAttribute(name = "verticalStepHeight")
340     public Integer getVerticalStepHeight() {
341         return verticalStepHeight;
342     }
343 
344     /**
345      * @see StepProgressBar#getVerticalStepHeight()
346      */
347     public void setVerticalStepHeight(Integer verticalStepHeight) {
348         this.verticalStepHeight = verticalStepHeight;
349     }
350 }