1 /**
2 * Copyright 2005-2012 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.lang.StringUtils;
19 import org.kuali.rice.core.api.exception.RiceRuntimeException;
20 import org.kuali.rice.krad.uif.UifConstants;
21 import org.kuali.rice.krad.uif.UifParameters;
22 import org.kuali.rice.krad.uif.UifPropertyPaths;
23 import org.kuali.rice.krad.uif.component.ComponentSecurity;
24 import org.kuali.rice.krad.uif.view.FormView;
25 import org.kuali.rice.krad.uif.view.View;
26 import org.kuali.rice.krad.uif.component.Component;
27
28 import java.util.HashMap;
29 import java.util.List;
30 import java.util.Map;
31
32 /**
33 * Field that presents an action that can be taken on the UI such as submitting
34 * the form or invoking a script
35 *
36 * @author Kuali Rice Team (rice.collab@kuali.org)
37 */
38 public class Action extends ContentElementBase {
39 private static final long serialVersionUID = 1025672792657238829L;
40
41 private String methodToCall;
42 private String actionEvent;
43 private String navigateToPageId;
44
45 private String actionScript;
46
47 private String actionLabel;
48 private Image actionImage;
49 private String actionImagePlacement;
50
51 private String jumpToIdAfterSubmit;
52 private String jumpToNameAfterSubmit;
53 private String focusOnIdAfterSubmit;
54
55 private boolean performClientSideValidation;
56 private boolean performDirtyValidation;
57
58 private boolean disabled;
59 private String disabledReason;
60
61 private String preSubmitCall;
62 private boolean ajaxSubmit;
63
64 private String successCallback;
65 private String errorCallback;
66
67 private String loadingMessageText;
68 private String updatingMessageText;
69
70 private Map<String, String> actionParameters;
71
72 public Action() {
73 super();
74
75 actionImagePlacement = UifConstants.Position.LEFT.name();
76
77 ajaxSubmit = true;
78 disabled = false;
79
80 successCallback = "";
81 errorCallback = "";
82 preSubmitCall = "";
83
84 actionParameters = new HashMap<String, String>();
85 }
86
87 /**
88 * The following finalization is performed:
89 *
90 * <ul>
91 * <li>Add methodToCall action parameter if set and setup event code for
92 * setting action parameters</li>
93 * <li>Invoke method to build the data attributes and submit data for the action</li>
94 * <li>Compose the final onclick script for the action</li>
95 * </ul>
96 *
97 * @see org.kuali.rice.krad.uif.component.ComponentBase#performFinalize(org.kuali.rice.krad.uif.view.View,
98 * java.lang.Object, org.kuali.rice.krad.uif.component.Component)
99 */
100 @Override
101 public void performFinalize(View view, Object model, Component parent) {
102 super.performFinalize(view, model, parent);
103
104 // clear alt text to avoid screen reader confusion when using image in button with text
105 if (actionImage != null && StringUtils.isNotBlank(actionImagePlacement) && StringUtils.isNotBlank(
106 actionLabel)) {
107 actionImage.setAltText("");
108 }
109
110 if (!actionParameters.containsKey(UifConstants.UrlParams.ACTION_EVENT) && StringUtils.isNotBlank(actionEvent)) {
111 actionParameters.put(UifConstants.UrlParams.ACTION_EVENT, actionEvent);
112 }
113
114 if (StringUtils.isNotBlank(navigateToPageId)) {
115 actionParameters.put(UifParameters.NAVIGATE_TO_PAGE_ID, navigateToPageId);
116 if (StringUtils.isBlank(methodToCall)) {
117 actionParameters.put(UifConstants.CONTROLLER_METHOD_DISPATCH_PARAMETER_NAME,
118 UifConstants.MethodToCallNames.NAVIGATE);
119 }
120 }
121
122 if (!actionParameters.containsKey(UifConstants.CONTROLLER_METHOD_DISPATCH_PARAMETER_NAME) && StringUtils
123 .isNotBlank(methodToCall)) {
124 actionParameters.put(UifConstants.CONTROLLER_METHOD_DISPATCH_PARAMETER_NAME, methodToCall);
125 }
126
127 buildActionData(view, model, parent);
128
129 // build final onclick script
130 String onClickScript = this.getOnClickScript();
131 if (onClickScript == null) {
132 onClickScript = "";
133 }
134
135 if (StringUtils.isNotBlank(actionScript)) {
136 onClickScript += actionScript;
137 } else {
138 onClickScript += "actionInvokeHandler(this);";
139 }
140
141 // add dirty check if it is enabled for the view and the action requires it
142 if (view instanceof FormView) {
143 if (((FormView) view).isApplyDirtyCheck() && performDirtyValidation) {
144 onClickScript = "if (checkDirty(e) == false) { " + onClickScript + " ; } ";
145 }
146 }
147
148 setOnClickScript("e.preventDefault();" + onClickScript);
149 }
150
151 /**
152 * Builds the data attributes that will be the will be read client side to determine how to
153 * handle the action and the additional data that should be submitted with the action
154 *
155 * <p>
156 * Note these data attributes will be exposed as a data map client side. The simple attributes (non object
157 * value) are also written out as attributes on the action element.
158 * </p>
159 *
160 * @param view - view instance the action belongs to
161 * @param model - model object containing the view data
162 * @param parent - component the holds the action
163 */
164 protected void buildActionData(View view, Object model, Component parent) {
165 // map properties to data attributes
166 addDataAttribute("ajaxsubmit", Boolean.toString(ajaxSubmit));
167 addDataAttributeIfNonEmpty("successcallback", this.successCallback);
168 addDataAttributeIfNonEmpty("errorcallback", this.errorCallback);
169 addDataAttributeIfNonEmpty("presubmitcall", this.preSubmitCall);
170 addDataAttributeIfNonEmpty("loadingMessageText", this.loadingMessageText);
171 addDataAttributeIfNonEmpty("updatingMessageText", this.updatingMessageText);
172 addDataAttribute("validate", Boolean.toString(this.performClientSideValidation));
173
174 // all action parameters should be submitted with action
175 Map<String, String> submitData = new HashMap<String, String>();
176 for (String key : actionParameters.keySet()) {
177 String parameterPath = key;
178 if (!key.equals(UifConstants.CONTROLLER_METHOD_DISPATCH_PARAMETER_NAME)) {
179 parameterPath = UifPropertyPaths.ACTION_PARAMETERS + "[" + key + "]";
180 }
181 submitData.put(parameterPath, actionParameters.get(key));
182 }
183
184 // TODO possibly fix some other way - this is a workaround, prevents
185 // showing history and showing home again on actions which submit the form
186 submitData.put(UifConstants.UrlParams.SHOW_HISTORY, "false");
187 submitData.put(UifConstants.UrlParams.SHOW_HOME, "false");
188
189 // if focus id not set default to focus on action
190 if (StringUtils.isBlank(focusOnIdAfterSubmit)) {
191 focusOnIdAfterSubmit = this.getId();
192 submitData.put("focusId", focusOnIdAfterSubmit);
193 } else if (!focusOnIdAfterSubmit.equalsIgnoreCase(UifConstants.Order.FIRST.toString())) {
194 // Use the id passed in
195 submitData.put("focusId", focusOnIdAfterSubmit);
196 }
197
198 // if jump to not set default to jump to location of the action
199 if (StringUtils.isBlank(jumpToIdAfterSubmit) && StringUtils.isBlank(jumpToNameAfterSubmit)) {
200 jumpToIdAfterSubmit = this.getId();
201 submitData.put("jumpToId", jumpToIdAfterSubmit);
202 } else if (StringUtils.isNotBlank(jumpToIdAfterSubmit)) {
203 submitData.put("jumpToId", jumpToIdAfterSubmit);
204 } else {
205 submitData.put("jumpToName", jumpToNameAfterSubmit);
206 }
207
208 addDataAttribute("submitData", mapToString(submitData));
209 }
210
211 /**
212 * @see org.kuali.rice.krad.uif.component.ComponentBase#getComponentsForLifecycle()
213 */
214 @Override
215 public List<Component> getComponentsForLifecycle() {
216 List<Component> components = super.getComponentsForLifecycle();
217
218 components.add(actionImage);
219
220 return components;
221 }
222
223 private String mapToString(Map<String, String> submitData) {
224 StringBuffer sb = new StringBuffer("{");
225 for (String key : submitData.keySet()) {
226 Object optionValue = submitData.get(key);
227 if (sb.length() > 1) {
228 sb.append(",");
229 }
230 sb.append("\"" + key + "\"");
231
232 sb.append(":");
233 sb.append("\"" + optionValue + "\"");
234 }
235 sb.append("}");
236 return sb.toString();
237 }
238
239 /**
240 * Name of the method that should be called when the action is selected
241 *
242 * <p>
243 * For a server side call (clientSideCall is false), gives the name of the
244 * method in the mapped controller that should be invoked when the action is
245 * selected. For client side calls gives the name of the script function
246 * that should be invoked when the action is selected
247 * </p>
248 *
249 * @return String name of method to call
250 */
251 public String getMethodToCall() {
252 return this.methodToCall;
253 }
254
255 /**
256 * Setter for the actions method to call
257 *
258 * @param methodToCall
259 */
260 public void setMethodToCall(String methodToCall) {
261 this.methodToCall = methodToCall;
262 }
263
264 /**
265 * Label text for the action
266 *
267 * <p>
268 * The label text is used by the template renderers to give a human readable
269 * label for the action. For buttons this generally is the button text,
270 * while for an action link it would be the links displayed text
271 * </p>
272 *
273 * @return String label for action
274 */
275 public String getActionLabel() {
276 return this.actionLabel;
277 }
278
279 /**
280 * Setter for the actions label
281 *
282 * @param actionLabel
283 */
284 public void setActionLabel(String actionLabel) {
285 this.actionLabel = actionLabel;
286 }
287
288 /**
289 * Image to use for the action
290 *
291 * <p>
292 * When the action image component is set (and render is true) the image will be
293 * used to present the action as opposed to the default (input submit). For
294 * action link templates the image is used for the link instead of the
295 * action link text
296 * </p>
297 *
298 * @return Image action image
299 */
300 public Image getActionImage() {
301 return this.actionImage;
302 }
303
304 /**
305 * Setter for the action image field
306 *
307 * @param actionImage
308 */
309 public void setActionImage(Image actionImage) {
310 this.actionImage = actionImage;
311 }
312
313 /**
314 * For an <code>Action</code> that is part of a
315 * <code>NavigationGroup</code, the navigate to page id can be set to
316 * configure the page that should be navigated to when the action is
317 * selected
318 *
319 * <p>
320 * Support exists in the <code>UifControllerBase</code> for handling
321 * navigation between pages
322 * </p>
323 *
324 * @return String id of page that should be rendered when the action item is
325 * selected
326 */
327 public String getNavigateToPageId() {
328 return this.navigateToPageId;
329 }
330
331 /**
332 * Setter for the navigate to page id
333 *
334 * @param navigateToPageId
335 */
336 public void setNavigateToPageId(String navigateToPageId) {
337 this.navigateToPageId = navigateToPageId;
338 actionParameters.put(UifParameters.NAVIGATE_TO_PAGE_ID, navigateToPageId);
339 this.methodToCall = UifConstants.MethodToCallNames.NAVIGATE;
340 }
341
342 /**
343 * Name of the event that will be set when the action is invoked
344 *
345 * <p>
346 * Action events can be looked at by the view or components in order to render differently depending on
347 * the action requested.
348 * </p>
349 *
350 * @return String action event name
351 * @see org.kuali.rice.krad.uif.UifConstants.ActionEvents
352 */
353 public String getActionEvent() {
354 return actionEvent;
355 }
356
357 /**
358 * Setter for the action event
359 *
360 * @param actionEvent
361 */
362 public void setActionEvent(String actionEvent) {
363 this.actionEvent = actionEvent;
364 }
365
366 /**
367 * Parameters that should be sent when the action is invoked
368 *
369 * <p>
370 * Action renderer will decide how the parameters are sent for the action
371 * (via script generated hiddens, or script parameters, ...)
372 * </p>
373 *
374 * <p>
375 * Can be set by other components such as the <code>CollectionGroup</code>
376 * to provide the context the action is in (such as the collection name and
377 * line the action applies to)
378 * </p>
379 *
380 * @return Map<String, String> action parameters
381 */
382 public Map<String, String> getActionParameters() {
383 return this.actionParameters;
384 }
385
386 /**
387 * Setter for the action parameters
388 *
389 * @param actionParameters
390 */
391 public void setActionParameters(Map<String, String> actionParameters) {
392 this.actionParameters = actionParameters;
393 }
394
395 /**
396 * Convenience method to add a parameter to the action parameters Map
397 *
398 * @param parameterName - name of parameter to add
399 * @param parameterValue - value of parameter to add
400 */
401 public void addActionParameter(String parameterName, String parameterValue) {
402 if (actionParameters == null) {
403 this.actionParameters = new HashMap<String, String>();
404 }
405
406 this.actionParameters.put(parameterName, parameterValue);
407 }
408
409 /**
410 * Get an actionParameter by name
411 */
412 public String getActionParameter(String parameterName) {
413 return this.actionParameters.get(parameterName);
414 }
415
416 /**
417 * Action Field Security object that indicates what authorization (permissions) exist for the action
418 *
419 * @return ActionSecurity instance
420 */
421 @Override
422 public ActionSecurity getComponentSecurity() {
423 return (ActionSecurity) super.getComponentSecurity();
424 }
425
426 /**
427 * Override to assert a {@link ActionSecurity} instance is set
428 *
429 * @param componentSecurity - instance of ActionSecurity
430 */
431 @Override
432 public void setComponentSecurity(ComponentSecurity componentSecurity) {
433 if (!(componentSecurity instanceof ActionSecurity)) {
434 throw new RiceRuntimeException("Component security for Action should be instance of ActionSecurity");
435 }
436
437 super.setComponentSecurity(componentSecurity);
438 }
439
440 @Override
441 protected Class<? extends ComponentSecurity> getComponentSecurityClass() {
442 return ActionSecurity.class;
443 }
444
445 /**
446 * @return the jumpToIdAfterSubmit
447 */
448 public String getJumpToIdAfterSubmit() {
449 return this.jumpToIdAfterSubmit;
450 }
451
452 /**
453 * The id to jump to in the next page, the element with this id will be
454 * jumped to automatically when the new page is retrieved after a submit.
455 * Using "TOP" or "BOTTOM" will jump to the top or the bottom of the
456 * resulting page. Passing in nothing for both jumpToIdAfterSubmit and
457 * jumpToNameAfterSubmit will result in this Action being jumped to by
458 * default if it is present on the new page. WARNING: jumpToIdAfterSubmit
459 * always takes precedence over jumpToNameAfterSubmit, if set.
460 *
461 * @param jumpToIdAfterSubmit the jumpToIdAfterSubmit to set
462 */
463 public void setJumpToIdAfterSubmit(String jumpToIdAfterSubmit) {
464 this.jumpToIdAfterSubmit = jumpToIdAfterSubmit;
465 }
466
467 /**
468 * The name to jump to in the next page, the element with this name will be
469 * jumped to automatically when the new page is retrieved after a submit.
470 * Passing in nothing for both jumpToIdAfterSubmit and jumpToNameAfterSubmit
471 * will result in this Action being jumped to by default if it is
472 * present on the new page. WARNING: jumpToIdAfterSubmit always takes
473 * precedence over jumpToNameAfterSubmit, if set.
474 *
475 * @return the jumpToNameAfterSubmit
476 */
477 public String getJumpToNameAfterSubmit() {
478 return this.jumpToNameAfterSubmit;
479 }
480
481 /**
482 * @param jumpToNameAfterSubmit the jumpToNameAfterSubmit to set
483 */
484 public void setJumpToNameAfterSubmit(String jumpToNameAfterSubmit) {
485 this.jumpToNameAfterSubmit = jumpToNameAfterSubmit;
486 }
487
488 /**
489 * The id of the field to place focus on in the new page after the new page
490 * is retrieved. Passing in "FIRST" will focus on the first visible input
491 * element on the form. Passing in the empty string will result in this
492 * Action being focused.
493 *
494 * @return the focusOnAfterSubmit
495 */
496 public String getFocusOnIdAfterSubmit() {
497 return this.focusOnIdAfterSubmit;
498 }
499
500 /**
501 * @param focusOnIdAfterSubmit the focusOnAfterSubmit to set
502 */
503 public void setFocusOnIdAfterSubmit(String focusOnIdAfterSubmit) {
504 this.focusOnIdAfterSubmit = focusOnIdAfterSubmit;
505 }
506
507 /**
508 * Indicates whether the form data should be validated on the client side
509 *
510 * return true if validation should occur, false otherwise
511 */
512 public boolean isPerformClientSideValidation() {
513 return this.performClientSideValidation;
514 }
515
516 /**
517 * Setter for the client side validation flag
518 *
519 * @param performClientSideValidation
520 */
521 public void setPerformClientSideValidation(boolean performClientSideValidation) {
522 this.performClientSideValidation = performClientSideValidation;
523 }
524
525 /**
526 * Client side javascript to be executed when this actionField is clicked
527 *
528 * <p>
529 * This overrides the default action for this Action so the method
530 * called must explicitly submit, navigate, etc. through js, if necessary.
531 * In addition, this js occurs AFTER onClickScripts set on this field, it
532 * will be the last script executed by the click event. Sidenote: This js is
533 * always called after hidden actionParameters and methodToCall methods are
534 * written by the js to the html form.
535 * </p>
536 *
537 * @return the actionScript
538 */
539 public String getActionScript() {
540 return this.actionScript;
541 }
542
543 /**
544 * @param actionScript the actionScript to set
545 */
546 public void setActionScript(String actionScript) {
547 if (!StringUtils.endsWith(actionScript, ";")) {
548 actionScript = actionScript + ";";
549 }
550 this.actionScript = actionScript;
551 }
552
553 /**
554 * @param performDirtyValidation the blockValidateDirty to set
555 */
556 public void setPerformDirtyValidation(boolean performDirtyValidation) {
557 this.performDirtyValidation = performDirtyValidation;
558 }
559
560 /**
561 * @return the blockValidateDirty
562 */
563 public boolean isPerformDirtyValidation() {
564 return performDirtyValidation;
565 }
566
567 /**
568 * Indicates whether the action (input or button) is disabled (doesn't allow interaction)
569 *
570 * @return boolean true if the action field is disabled, false if not
571 */
572 public boolean isDisabled() {
573 return disabled;
574 }
575
576 /**
577 * Setter for the disabled indicator
578 *
579 * @param disabled
580 */
581 public void setDisabled(boolean disabled) {
582 this.disabled = disabled;
583 }
584
585 /**
586 * If the action field is disabled, gives a reason for why which will be displayed as a tooltip
587 * on the action field (button)
588 *
589 * @return String disabled reason text
590 * @see {@link #isDisabled()}
591 */
592 public String getDisabledReason() {
593 return disabledReason;
594 }
595
596 /**
597 * Setter for the disabled reason text
598 *
599 * @param disabledReason
600 */
601 public void setDisabledReason(String disabledReason) {
602 this.disabledReason = disabledReason;
603 }
604
605 public String getActionImagePlacement() {
606 return actionImagePlacement;
607 }
608
609 /**
610 * Set to TOP, BOTTOM, LEFT, RIGHT to position image at that location within the button.
611 * For the subclass ActionLinkField only LEFT and RIGHT are allowed. When set to blank/null/IMAGE_ONLY, the image
612 * itself will be the Action, if no value is set the default is ALWAYS LEFT, you must explicitly set
613 * blank/null/IMAGE_ONLY to use ONLY the image as the Action.
614 *
615 * @return
616 */
617 public void setActionImagePlacement(String actionImagePlacement) {
618 this.actionImagePlacement = actionImagePlacement;
619 }
620
621 /**
622 * Gets the script which needs to be invoked before the form is submitted. The script should return a boolean
623 * indicating if the form should be submitted or not.
624 *
625 * @return String script text that will be invoked before form submission
626 */
627 public String getPreSubmitCall() {
628 return preSubmitCall;
629 }
630
631 /**
632 * Setter for preSubmitCall
633 *
634 * @param preSubmitCall
635 */
636 public void setPreSubmitCall(String preSubmitCall) {
637 this.preSubmitCall = preSubmitCall;
638 }
639
640 /**
641 * When this property is set to true it will submit the form using Ajax instead of the browser submit. Will default
642 * to updating the page contents
643 *
644 * @return boolean
645 */
646 public boolean isAjaxSubmit() {
647 return ajaxSubmit;
648 }
649
650 /**
651 * Setter for ajaxSubmit
652 *
653 * @param ajaxSubmit
654 */
655 public void setAjaxSubmit(boolean ajaxSubmit) {
656 this.ajaxSubmit = ajaxSubmit;
657 }
658
659 /**
660 * get loading message used by action's blockUI
661 *
662 * @returns String if String is not null, used in place of loading message
663 */
664 public String getLoadingMessageText() {
665 return loadingMessageText;
666 }
667
668 /**
669 * When this property is set, it is used in place of the loading message text used by the blockUI
670 *
671 * @param loadingMessageText
672 */
673 public void setLoadingMessageText(String loadingMessageText) {
674 this.loadingMessageText = loadingMessageText;
675 }
676
677 /**
678 * get updating message used by action's blockUI
679 *
680 * @returns String if String is not null, used in place of updating message
681 */
682 public String getUpdatingMessageText() {
683 return updatingMessageText;
684 }
685
686 /**
687 * When this property is set, it is used in place of the updating message text used by the blockUI
688 *
689 * @param updatingMessageText
690 */
691 public void setUpdatingMessageText(String updatingMessageText) {
692 this.updatingMessageText = updatingMessageText;
693 }
694
695 /**
696 * Getter for successCallback property. This will be invoked for successful ajax calls
697 *
698 * @return String
699 */
700
701 public String getSuccessCallback() {
702 return successCallback;
703 }
704
705 /**
706 * Setter for successCallback
707 *
708 * @param successCallback
709 */
710 public void setSuccessCallback(String successCallback) {
711 this.successCallback = successCallback;
712 }
713
714 /**
715 * Getter for errorCallback. This will be invoked for failed ajax calls
716 *
717 * @return
718 */
719 public String getErrorCallback() {
720 return errorCallback;
721 }
722
723 /**
724 * Setter for errorCallback
725 *
726 * @param errorCallback
727 */
728 public void setErrorCallback(String errorCallback) {
729 this.errorCallback = errorCallback;
730 }
731 }