001/**
002 * Copyright 2005-2016 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.field;
017
018import org.apache.commons.lang.StringUtils;
019import org.kuali.rice.core.api.exception.RiceRuntimeException;
020import org.kuali.rice.krad.uif.UifConstants;
021import org.kuali.rice.krad.uif.UifParameters;
022import org.kuali.rice.krad.uif.UifPropertyPaths;
023import org.kuali.rice.krad.uif.component.ComponentSecurity;
024import org.kuali.rice.krad.uif.view.FormView;
025import org.kuali.rice.krad.uif.view.View;
026import org.kuali.rice.krad.uif.component.Component;
027import org.kuali.rice.krad.uif.widget.LightBox;
028
029import java.util.HashMap;
030import java.util.List;
031import java.util.Map;
032
033/**
034 * Field that presents an action that can be taken on the UI such as submitting
035 * the form or invoking a script
036 * 
037 * @author Kuali Rice Team (rice.collab@kuali.org)
038 */
039public class ActionField extends FieldBase {
040    private static final long serialVersionUID = 1025672792657238829L;
041
042    private String methodToCall;
043    private String navigateToPageId;
044
045    private boolean clientSideValidate;
046    private String clientSideJs;
047
048    private String jumpToIdAfterSubmit;
049    private String jumpToNameAfterSubmit;
050    private String focusOnAfterSubmit;
051
052    private String actionLabel;
053    private ImageField actionImage;
054    private String actionImageLocation = "LEFT";
055
056    private String actionEvent;
057    private Map<String, String> actionParameters;
058
059    private LightBox lightBoxLookup;
060    private LightBox lightBoxDirectInquiry;
061
062    private boolean blockValidateDirty;
063    private boolean disabled;
064    private String disabledReason;
065
066    public ActionField() {
067        super();
068
069        disabled = false;
070
071        actionParameters = new HashMap<String, String>();
072    }
073
074    /**
075     * The following initialization is performed:
076     *
077     * <ul>
078     * <li>Set the actionLabel if blank to the Field label</li>
079     * </ul>
080     * 
081     * @see org.kuali.rice.krad.uif.component.ComponentBase#performInitialization(org.kuali.rice.krad.uif.view.View, java.lang.Object)
082     */
083    @Override
084    public void performInitialization(View view, Object model) {
085        super.performInitialization(view, model);
086
087        if (StringUtils.isBlank(actionLabel)) {
088            actionLabel = this.getLabel();
089        }
090    }
091
092    /**
093     * The following finalization is performed:
094     *
095     * <ul>
096     * <li>Add methodToCall action parameter if set and setup event code for
097     * setting action parameters</li>
098     * </ul>
099     * 
100     * @see org.kuali.rice.krad.uif.component.ComponentBase#performFinalize(org.kuali.rice.krad.uif.view.View,
101     *      java.lang.Object, org.kuali.rice.krad.uif.component.Component)
102     */
103    @Override
104    public void performFinalize(View view, Object model, Component parent) {
105        super.performFinalize(view, model, parent);
106        //clear alt text to avoid screen reader confusion when using image in button with text
107        if(actionImage != null && StringUtils.isNotBlank(actionImageLocation) && StringUtils.isNotBlank(actionLabel)){
108            actionImage.setAltText("");
109        }
110
111        if (!actionParameters.containsKey(UifConstants.UrlParams.ACTION_EVENT) && StringUtils.isNotBlank(actionEvent)) {
112            actionParameters.put(UifConstants.UrlParams.ACTION_EVENT, actionEvent);
113        }
114
115        actionParameters.put(UifConstants.UrlParams.SHOW_HOME, "false");
116        actionParameters.put(UifConstants.UrlParams.SHOW_HISTORY, "false");
117
118        if (StringUtils.isNotBlank(navigateToPageId)) {
119            actionParameters.put(UifParameters.NAVIGATE_TO_PAGE_ID, navigateToPageId);
120            if (StringUtils.isBlank(methodToCall)) {
121                actionParameters.put(UifConstants.CONTROLLER_METHOD_DISPATCH_PARAMETER_NAME,
122                        UifConstants.MethodToCallNames.NAVIGATE);
123            }
124        }
125
126        if (!actionParameters.containsKey(UifConstants.CONTROLLER_METHOD_DISPATCH_PARAMETER_NAME)
127                && StringUtils.isNotBlank(methodToCall)) {
128            actionParameters.put(UifConstants.CONTROLLER_METHOD_DISPATCH_PARAMETER_NAME, methodToCall);
129        }
130
131        // If there is no lightBox then create the on click script
132        if (lightBoxLookup == null) {
133            String prefixScript = this.getOnClickScript();
134            if (prefixScript == null) {
135                prefixScript = "";
136            }
137
138            boolean validateFormDirty = false;
139            if (view instanceof FormView && !isBlockValidateDirty()) {
140                validateFormDirty = ((FormView) view).isValidateDirty();
141            }
142
143            boolean includeDirtyCheckScript = false;
144            String writeParamsScript = "";
145            if (!actionParameters.isEmpty()) {
146                for (String key : actionParameters.keySet()) {
147                    String parameterPath = key;
148                    if (!key.equals(UifConstants.CONTROLLER_METHOD_DISPATCH_PARAMETER_NAME)) {
149                        parameterPath = UifPropertyPaths.ACTION_PARAMETERS + "[" + key + "]";
150                    }
151
152                    writeParamsScript = writeParamsScript + "writeHiddenToForm('" + parameterPath + "' , '"
153                            + actionParameters.get(key) + "'); ";
154
155                    // Include dirtycheck js function call if the method to call
156                    // is refresh, navigate, cancel or close
157                    if (validateFormDirty && !includeDirtyCheckScript
158                            && key.equals(UifConstants.CONTROLLER_METHOD_DISPATCH_PARAMETER_NAME)) {
159                        String keyValue = (String) actionParameters.get(key);
160                        if (StringUtils.equals(keyValue, UifConstants.MethodToCallNames.REFRESH)
161                                || StringUtils.equals(keyValue, UifConstants.MethodToCallNames.NAVIGATE)
162                                || StringUtils.equals(keyValue, UifConstants.MethodToCallNames.CANCEL)
163                                || StringUtils.equals(keyValue, UifConstants.MethodToCallNames.CLOSE)) {
164                            includeDirtyCheckScript = true;
165                        }
166                    }
167                }
168            }
169
170            // TODO possibly fix some other way - this is a workaround, prevents
171            // showing history and showing home again on actions which submit
172            // the form
173            writeParamsScript = writeParamsScript + "writeHiddenToForm('" + UifConstants.UrlParams.SHOW_HISTORY
174                    + "', '" + "false" + "'); ";
175            writeParamsScript = writeParamsScript + "writeHiddenToForm('" + UifConstants.UrlParams.SHOW_HOME + "' , '"
176                    + "false" + "'); ";
177
178            if (StringUtils.isBlank(focusOnAfterSubmit)) {
179                // if this is blank focus this actionField by default
180                focusOnAfterSubmit = this.getId();
181                writeParamsScript = writeParamsScript + "writeHiddenToForm('focusId' , '" + this.getId() + "'); ";
182            } else if (!focusOnAfterSubmit.equalsIgnoreCase(UifConstants.Order.FIRST.toString())) {
183                // Use the id passed in
184                writeParamsScript = writeParamsScript + "writeHiddenToForm('focusId' , '" + focusOnAfterSubmit + "'); ";
185            } else {
186                // First input will be focused, must be first field set to empty
187                // string
188                writeParamsScript = writeParamsScript + "writeHiddenToForm('focusId' , ''); ";
189            }
190
191            if (StringUtils.isBlank(jumpToIdAfterSubmit) && StringUtils.isBlank(jumpToNameAfterSubmit)) {
192                jumpToIdAfterSubmit = this.getId();
193                writeParamsScript = writeParamsScript + "writeHiddenToForm('jumpToId' , '" + this.getId() + "'); ";
194            } else if (StringUtils.isNotBlank(jumpToIdAfterSubmit)) {
195                writeParamsScript = writeParamsScript + "writeHiddenToForm('jumpToId' , '" + jumpToIdAfterSubmit
196                        + "'); ";
197            } else {
198                writeParamsScript = writeParamsScript + "writeHiddenToForm('jumpToName' , '" + jumpToNameAfterSubmit
199                        + "'); ";
200            }
201            
202            String postScript = "";
203            if (StringUtils.isNotBlank(clientSideJs)) {
204                postScript = clientSideJs;
205            }
206            if (isClientSideValidate()) {
207                postScript = postScript + "validateAndSubmitUsingFormMethodToCall();";
208            }
209            if (StringUtils.isBlank(postScript)) {
210                postScript = "writeHiddenToForm('renderFullView' , 'true'); jq('#kualiForm').submit();";
211            }
212
213            if (includeDirtyCheckScript) {
214                this.setOnClickScript("e.preventDefault(); if (checkDirty(e) == false) { " + prefixScript
215                        + writeParamsScript + postScript + " ; } ");
216            } else {
217                this.setOnClickScript("e.preventDefault();" + prefixScript + writeParamsScript + postScript);
218            }
219
220        } else {
221            // When there is a light box - don't add the on click script as it
222            // will be prevented from executing
223            // Create a script map object which will be written to the form on
224            // click event
225            StringBuffer sb = new StringBuffer();
226            sb.append("{");
227            for (String key : actionParameters.keySet()) {
228                String optionValue = actionParameters.get(key);
229                if (sb.length() > 1) {
230                    sb.append(",");
231                }
232                if (!key.equals(UifConstants.CONTROLLER_METHOD_DISPATCH_PARAMETER_NAME)) {
233                    sb.append("\"" + UifPropertyPaths.ACTION_PARAMETERS + "[" + key + "]" + "\"");
234                } else {
235                    sb.append("\"" + key + "\"");
236                }
237                sb.append(":");
238                sb.append("\"" + optionValue + "\"");
239            }
240            sb.append("}");
241            lightBoxLookup.setActionParameterMapString(sb.toString());
242        }
243    }
244
245    /**
246     * @see org.kuali.rice.krad.uif.component.ComponentBase#getComponentsForLifecycle()
247     */
248    @Override
249    public List<Component> getComponentsForLifecycle() {
250        List<Component> components = super.getComponentsForLifecycle();
251
252        components.add(actionImage);
253        components.add(lightBoxLookup);
254        components.add(lightBoxDirectInquiry);
255
256        return components;
257    }
258
259    /**
260     * Name of the method that should be called when the action is selected
261     * <p>
262     * For a server side call (clientSideCall is false), gives the name of the
263     * method in the mapped controller that should be invoked when the action is
264     * selected. For client side calls gives the name of the script function
265     * that should be invoked when the action is selected
266     * </p>
267     * 
268     * @return String name of method to call
269     */
270    public String getMethodToCall() {
271        return this.methodToCall;
272    }
273
274    /**
275     * Setter for the actions method to call
276     * 
277     * @param methodToCall
278     */
279    public void setMethodToCall(String methodToCall) {
280        this.methodToCall = methodToCall;
281    }
282
283    /**
284     * Label text for the action
285     * <p>
286     * The label text is used by the template renderers to give a human readable
287     * label for the action. For buttons this generally is the button text,
288     * while for an action link it would be the links displayed text
289     * </p>
290     * 
291     * @return String label for action
292     */
293    public String getActionLabel() {
294        return this.actionLabel;
295    }
296
297    /**
298     * Setter for the actions label
299     * 
300     * @param actionLabel
301     */
302    public void setActionLabel(String actionLabel) {
303        this.actionLabel = actionLabel;
304    }
305
306    /**
307     * Image to use for the action
308     * <p>
309     * When the action image field is set (and render is true) the image will be
310     * used to present the action as opposed to the default (input submit). For
311     * action link templates the image is used for the link instead of the
312     * action link text
313     * </p>
314     * 
315     * @return ImageField action image
316     */
317    public ImageField getActionImage() {
318        return this.actionImage;
319    }
320
321    /**
322     * Setter for the action image field
323     * 
324     * @param actionImage
325     */
326    public void setActionImage(ImageField actionImage) {
327        this.actionImage = actionImage;
328    }
329
330    /**
331     * For an <code>ActionField</code> that is part of a
332     * <code>NavigationGroup</code, the navigate to page id can be set to
333     * configure the page that should be navigated to when the action is
334     * selected
335     * <p>
336     * Support exists in the <code>UifControllerBase</code> for handling
337     * navigation between pages
338     * </p>
339     * 
340     * @return String id of page that should be rendered when the action item is
341     *         selected
342     */
343    public String getNavigateToPageId() {
344        return this.navigateToPageId;
345    }
346
347    /**
348     * Setter for the navigate to page id
349     * 
350     * @param navigateToPageId
351     */
352    public void setNavigateToPageId(String navigateToPageId) {
353        this.navigateToPageId = navigateToPageId;
354        actionParameters.put(UifParameters.NAVIGATE_TO_PAGE_ID, navigateToPageId);
355        this.methodToCall = UifConstants.MethodToCallNames.NAVIGATE;
356    }
357
358    /**
359     * Name of the event that will be set when the action is invoked
360     *
361     * <p>
362     * Action events can be looked at by the view or components in order to render differently depending on
363     * the action requested.
364     * </p>
365     *
366     * @return String action event name
367     * @see org.kuali.rice.krad.uif.UifConstants.ActionEvents
368     */
369    public String getActionEvent() {
370        return actionEvent;
371    }
372
373    /**
374     * Setter for the action event
375     *
376     * @param actionEvent
377     */
378    public void setActionEvent(String actionEvent) {
379        this.actionEvent = actionEvent;
380    }
381
382    /**
383     * Parameters that should be sent when the action is invoked
384     * <p>
385     * Action renderer will decide how the parameters are sent for the action
386     * (via script generated hiddens, or script parameters, ...)
387     * </p>
388     * <p>
389     * Can be set by other components such as the <code>CollectionGroup</code>
390     * to provide the context the action is in (such as the collection name and
391     * line the action applies to)
392     * </p>
393     * 
394     * @return Map<String, String> action parameters
395     */
396    public Map<String, String> getActionParameters() {
397        return this.actionParameters;
398    }
399
400    /**
401     * Setter for the action parameters
402     * 
403     * @param actionParameters
404     */
405    public void setActionParameters(Map<String, String> actionParameters) {
406        this.actionParameters = actionParameters;
407    }
408
409    /**
410     * Convenience method to add a parameter to the action parameters Map
411     * 
412     * @param parameterName
413     *            - name of parameter to add
414     * @param parameterValue
415     *            - value of parameter to add
416     */
417    public void addActionParameter(String parameterName, String parameterValue) {
418        if (actionParameters == null) {
419            this.actionParameters = new HashMap<String, String>();
420        }
421
422        this.actionParameters.put(parameterName, parameterValue);
423    }
424
425    /**
426     * Get an actionParameter by name
427     */
428    public String getActionParameter(String parameterName) {
429        return this.actionParameters.get(parameterName);
430    }
431
432    /**
433     * Action Field Security object that indicates what authorization (permissions) exist for the action
434     *
435     * @return ActionFieldSecurity instance
436     */
437    public ActionFieldSecurity getActionFieldSecurity() {
438        return (ActionFieldSecurity) super.getComponentSecurity();
439    }
440
441    /**
442     * Override to assert a {@link ActionFieldSecurity} instance is set
443     *
444     * @param componentSecurity - instance of ActionFieldSecurity
445     */
446    @Override
447    public void setComponentSecurity(ComponentSecurity componentSecurity) {
448        if (!(componentSecurity instanceof ActionFieldSecurity)) {
449            throw new RiceRuntimeException(
450                    "Component security for ActionField should be instance of ActionFieldSecurity");
451        }
452
453        super.setComponentSecurity(componentSecurity);
454    }
455
456    @Override
457    protected Class<? extends ComponentSecurity> getComponentSecurityClass() {
458        return ActionFieldSecurity.class;
459    }
460
461    /**
462     * @see org.kuali.rice.krad.uif.component.ComponentBase#getSupportsOnClick()
463     */
464    @Override
465    public boolean getSupportsOnClick() {
466        return true;
467    }
468
469    /**
470     * Setter for the light box lookup widget
471     * 
472     * @param lightBoxLookup
473     *            <code>LightBoxLookup</code> widget to set
474     */
475    public void setLightBoxLookup(LightBox lightBoxLookup) {
476        this.lightBoxLookup = lightBoxLookup;
477    }
478
479    /**
480     * LightBoxLookup widget for the field
481     * <p>
482     * The light box lookup widget will change the lookup behaviour to open the
483     * lookup in a light box.
484     * </p>
485     * 
486     * @return the <code>DirectInquiry</code> field DirectInquiry
487     */
488    public LightBox getLightBoxLookup() {
489        return lightBoxLookup;
490    }
491
492    /**
493     * @return the jumpToIdAfterSubmit
494     */
495    public String getJumpToIdAfterSubmit() {
496        return this.jumpToIdAfterSubmit;
497    }
498
499    /**
500     * The id to jump to in the next page, the element with this id will be
501     * jumped to automatically when the new page is retrieved after a submit.
502     * Using "TOP" or "BOTTOM" will jump to the top or the bottom of the
503     * resulting page. Passing in nothing for both jumpToIdAfterSubmit and
504     * jumpToNameAfterSubmit will result in this ActionField being jumped to by
505     * default if it is present on the new page. WARNING: jumpToIdAfterSubmit
506     * always takes precedence over jumpToNameAfterSubmit, if set.
507     * 
508     * @param jumpToIdAfterSubmit
509     *            the jumpToIdAfterSubmit to set
510     */
511    public void setJumpToIdAfterSubmit(String jumpToIdAfterSubmit) {
512        this.jumpToIdAfterSubmit = jumpToIdAfterSubmit;
513    }
514
515    /**
516     * The name to jump to in the next page, the element with this name will be
517     * jumped to automatically when the new page is retrieved after a submit.
518     * Passing in nothing for both jumpToIdAfterSubmit and jumpToNameAfterSubmit
519     * will result in this ActionField being jumped to by default if it is
520     * present on the new page. WARNING: jumpToIdAfterSubmit always takes
521     * precedence over jumpToNameAfterSubmit, if set.
522     * 
523     * @return the jumpToNameAfterSubmit
524     */
525    public String getJumpToNameAfterSubmit() {
526        return this.jumpToNameAfterSubmit;
527    }
528
529    /**
530     * @param jumpToNameAfterSubmit
531     *            the jumpToNameAfterSubmit to set
532     */
533    public void setJumpToNameAfterSubmit(String jumpToNameAfterSubmit) {
534        this.jumpToNameAfterSubmit = jumpToNameAfterSubmit;
535    }
536
537    /**
538     * The id of the field to place focus on in the new page after the new page
539     * is retrieved. Passing in "FIRST" will focus on the first visible input
540     * element on the form. Passing in the empty string will result in this
541     * ActionField being focused.
542     * 
543     * @return the focusOnAfterSubmit
544     */
545    public String getFocusOnAfterSubmit() {
546        return this.focusOnAfterSubmit;
547    }
548
549    /**
550     * @param focusOnAfterSubmit
551     *            the focusOnAfterSubmit to set
552     */
553    public void setFocusOnAfterSubmit(String focusOnAfterSubmit) {
554        this.focusOnAfterSubmit = focusOnAfterSubmit;
555    }
556
557    /**
558     * Indicates whether the form data should be validated on the client side
559     *
560     * return true if validation should occur, false otherwise
561     */
562    public boolean isClientSideValidate() {
563        return this.clientSideValidate;
564    }
565
566    /**
567     * Setter for the client side validation flag
568     * @param clientSideValidate
569     */
570    public void setClientSideValidate(boolean clientSideValidate) {
571        this.clientSideValidate = clientSideValidate;
572    }
573
574    /**
575     * Client side javascript to be executed when this actionField is clicked.
576     * This overrides the default action for this ActionField so the method
577     * called must explicitly submit, navigate, etc. through js, if necessary.
578     * In addition, this js occurs AFTER onClickScripts set on this field, it
579     * will be the last script executed by the click event. Sidenote: This js is
580     * always called after hidden actionParameters and methodToCall methods are
581     * written by the js to the html form.
582     * 
583     * @return the clientSideJs
584     */
585    public String getClientSideJs() {
586        return this.clientSideJs;
587    }
588
589    /**
590     * @param clientSideJs
591     *            the clientSideJs to set
592     */
593    public void setClientSideJs(String clientSideJs) {
594        if (!StringUtils.endsWith(clientSideJs, ";")) {
595            clientSideJs = clientSideJs + ";";
596        }
597        this.clientSideJs = clientSideJs;
598    }
599
600    /**
601     * Setter for the light box direct inquiry widget
602     * 
603     * @param lightBoxDirectInquiry
604     *            <code>LightBox</code> widget to set
605     */
606    public void setLightBoxDirectInquiry(LightBox lightBoxDirectInquiry) {
607        this.lightBoxDirectInquiry = lightBoxDirectInquiry;
608    }
609
610    /**
611     * LightBox widget for the field
612     * <p>
613     * The light box widget will change the direct inquiry behaviour to open up
614     * in a light box.
615     * </p>
616     * 
617     * @return the <code>LightBox</code> field LightBox
618     */
619    public LightBox getLightBoxDirectInquiry() {
620        return lightBoxDirectInquiry;
621    }
622
623    /**
624     * @param blockValidateDirty
625     *            the blockValidateDirty to set
626     */
627    public void setBlockValidateDirty(boolean blockValidateDirty) {
628        this.blockValidateDirty = blockValidateDirty;
629    }
630
631    /**
632     * @return the blockValidateDirty
633     */
634    public boolean isBlockValidateDirty() {
635        return blockValidateDirty;
636    }
637
638    /**
639     * Indicates whether the action (input or button) is disabled (doesn't allow interaction)
640     *
641     * @return boolean true if the action field is disabled, false if not
642     */
643    public boolean isDisabled() {
644        return disabled;
645    }
646
647    /**
648     * If the action field is disabled, gives a reason for why which will be displayed as a tooltip
649     * on the action field (button)
650     *
651     * @return String disabled reason text
652     * @see {@link #isDisabled()}
653     */
654    public String getDisabledReason() {
655        return disabledReason;
656    }
657
658    /**
659     * Setter for the disabled reason text
660     *
661     * @param disabledReason
662     */
663    public void setDisabledReason(String disabledReason) {
664        this.disabledReason = disabledReason;
665    }
666
667    /**
668     * Setter for the disabled indicator
669     *
670     * @param disabled
671     */
672    public void setDisabled(boolean disabled) {
673        this.disabled = disabled;
674    }
675
676    public String getActionImageLocation() {
677        return actionImageLocation;
678    }
679
680    /**
681     * Set to TOP, BOTTOM, LEFT, RIGHT to position image at that location within the button.
682     * For the subclass ActionLinkField only LEFT and RIGHT are allowed.  When set to blank/null/IMAGE_ONLY, the image
683     * itself will be the ActionField, if no value is set the default is ALWAYS LEFT, you must explicitly set
684     * blank/null/IMAGE_ONLY to use ONLY the image as the ActionField.
685     * @return
686     */
687    public void setActionImageLocation(String actionImageLocation) {
688        this.actionImageLocation = actionImageLocation;
689    }
690}