001/** 002 * Copyright 2005-2014 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.krad.uif.element; 017 018import org.apache.commons.collections.CollectionUtils; 019import org.apache.commons.lang3.StringUtils; 020import org.kuali.rice.krad.datadictionary.parse.BeanTag; 021import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute; 022import org.kuali.rice.krad.messages.MessageService; 023import org.kuali.rice.krad.service.KRADServiceLocatorWeb; 024import org.kuali.rice.krad.uif.CssConstants; 025import org.kuali.rice.krad.uif.UifConstants; 026import org.kuali.rice.krad.uif.util.LifecycleElement; 027 028import java.util.ArrayList; 029import java.util.Collection; 030import java.util.Iterator; 031import java.util.LinkedHashMap; 032import java.util.List; 033import java.util.Map; 034 035/** 036 * Element which shows a visual progress bar alongside a set of steps, to be used by wizard or multi-step 037 * processes, which reflects the current progress based on value of currentStep. 038 * 039 * @author Kuali Rice Team (rice.collab@kuali.org) 040 */ 041@BeanTag(name = "stepProgressBar-bean", parent = "Uif-StepProgressBar") 042public class StepProgressBar extends ProgressBar { 043 private static final long serialVersionUID = 1053164737424481519L; 044 045 private Map<String, String> steps; 046 private List<String> stepLabelClasses; 047 private List<String> accessibilityText; 048 049 private String currentStep; 050 private String completeStep; 051 052 private Integer verticalHeight; 053 private Integer verticalStepHeight; 054 055 public StepProgressBar() { 056 steps = new LinkedHashMap<String, String>(); 057 accessibilityText = new ArrayList<String>(); 058 stepLabelClasses = new ArrayList<String>(); 059 } 060 061 /** 062 * Populates the stepLabelClasses, accessibilityText, segmentSizes, and segmentClasses lists based on the settings 063 * of this StepProgressBar. 064 * 065 * {@inheritDoc} 066 */ 067 @Override 068 public void performFinalize(Object model, LifecycleElement parent) { 069 // If a percentageComplete value is set, use it to try to determine the current step, otherwise if currentStep 070 // is set just use that (null percentComplete value) 071 if (this.getPercentComplete() != null && currentStep == null) { 072 calculateCurrentStepFromPercentage(); 073 } else if (currentStep != null) { 074 this.setPercentComplete(null); 075 } 076 077 super.performFinalize(model, parent); 078 079 MessageService messageService = KRADServiceLocatorWeb.getMessageService(); 080 081 // Initializing and checking for validity of values: 082 String cssDimension = CssConstants.WIDTH; 083 if (this.isVertical()) { 084 cssDimension = CssConstants.HEIGHT; 085 this.addStyleClass(CssConstants.ProgressBar.VERTICAL_STEP_PROGRESS_BAR); 086 } 087 088 int totalSteps = steps.size(); 089 if (totalSteps == 0) { 090 throw new RuntimeException( 091 "At least one step is required for a StepProgressBar: " + this.getId() + " with parent: " + parent 092 .getId()); 093 } 094 095 boolean explicitlySetPercentages = CollectionUtils.isNotEmpty(getSegmentPercentages()); 096 boolean explicitlySetClasses = CollectionUtils.isNotEmpty(this.getSegmentClasses()); 097 if (explicitlySetPercentages && explicitlySetClasses && this.getSegmentClasses().size() != this 098 .getSegmentPercentages().size()) { 099 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}