001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.extensions.ajax.markup.html;
018
019import java.io.Serializable;
020
021import org.apache.wicket.Component;
022import org.apache.wicket.IGenericComponent;
023import org.apache.wicket.MarkupContainer;
024import org.apache.wicket.ajax.AbstractDefaultAjaxBehavior;
025import org.apache.wicket.ajax.AjaxEventBehavior;
026import org.apache.wicket.ajax.AjaxRequestTarget;
027import org.apache.wicket.ajax.attributes.AjaxCallListener;
028import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
029import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
030import org.apache.wicket.core.util.string.JavaScriptUtils;
031import org.apache.wicket.feedback.FeedbackMessage;
032import org.apache.wicket.markup.ComponentTag;
033import org.apache.wicket.markup.MarkupStream;
034import org.apache.wicket.markup.head.IHeaderResponse;
035import org.apache.wicket.markup.html.basic.Label;
036import org.apache.wicket.markup.html.form.FormComponent;
037import org.apache.wicket.markup.html.form.TextField;
038import org.apache.wicket.markup.html.panel.Panel;
039import org.apache.wicket.model.IModel;
040import org.apache.wicket.model.IObjectClassAwareModel;
041import org.apache.wicket.request.cycle.RequestCycle;
042import org.apache.wicket.util.convert.IConverter;
043import org.apache.wicket.util.string.Strings;
044import org.apache.wicket.validation.IValidator;
045
046/**
047 * An implementation of ajaxified edit-in-place component using a {@link TextField} as it's editor.
048 * <p>
049 * There are several methods that can be overridden for customization.
050 * <ul>
051 * <li>{@link #onEdit(org.apache.wicket.ajax.AjaxRequestTarget)} is called when the label is clicked
052 * and the editor is to be displayed. The default implementation switches the label for the editor
053 * and places the caret at the end of the text.</li>
054 * <li>{@link #onSubmit(org.apache.wicket.ajax.AjaxRequestTarget)} is called when in edit mode, the
055 * user submitted new content, that content validated well, and the model value successfully
056 * updated. This implementation also clears any <code>window.status</code> set.</li>
057 * <li>{@link #onError(org.apache.wicket.ajax.AjaxRequestTarget)} is called when in edit mode, the
058 * user submitted new content, but that content did not validate. Get the current input by calling
059 * {@link FormComponent#getInput()} on {@link #getEditor()}, and the error message by calling:
060 * 
061 * <pre>
062 * String errorMessage = editor.getFeedbackMessage().getMessage();
063 * </pre>
064 * 
065 * The default implementation of this method displays the error message in
066 * <code>window.status</code>, redisplays the editor, selects the editor's content and sets the
067 * focus on it.
068 * <li>{@link #onCancel(org.apache.wicket.ajax.AjaxRequestTarget)} is called when in edit mode, the
069 * user choose not to submit the contents (he/she pressed escape). The default implementation
070 * displays the label again without any further action.</li>
071 * </ul>
072 * </p>
073 * 
074 * @author Igor Vaynberg (ivaynberg)
075 * @author Eelco Hillenius
076 * @param <T>
077 */
078// TODO wonder if it makes sense to refactor this into a formcomponentpanel
079public class AjaxEditableLabel<T> extends Panel implements IGenericComponent<T, AjaxEditableLabel<T>>
080{
081        private static final long serialVersionUID = 1L;
082
083        /** editor component. */
084        private FormComponent<T> editor;
085
086        /** label component. */
087        private Component label;
088
089        protected class EditorAjaxBehavior extends AbstractDefaultAjaxBehavior
090        {
091                private static final long serialVersionUID = 1L;
092
093                @Override
094                protected void updateAjaxAttributes(AjaxRequestAttributes attributes)
095                {
096                        super.updateAjaxAttributes(attributes);
097
098                        AjaxEditableLabel.this.updateEditorAjaxAttributes(attributes);
099                }
100
101                @Override
102                public void renderHead(final Component component, final IHeaderResponse response)
103                {
104                        super.renderHead(component, response);
105
106                        getRequestCycle().find(IPartialPageRequestHandler.class).ifPresent(target -> target.appendJavaScript(getCallbackScript(component)));
107                }
108
109                @Override
110                protected void respond(final AjaxRequestTarget target)
111                {
112                        RequestCycle requestCycle = RequestCycle.get();
113                        boolean save = requestCycle.getRequest()
114                                .getRequestParameters()
115                                .getParameterValue("save")
116                                .toBoolean(false);
117
118                        if (save)
119                        {
120                                editor.processInput();
121
122                                if (editor.isValid())
123                                {
124                                        onSubmit(target);
125                                }
126                                else
127                                {
128                                        onError(target);
129                                }
130                        }
131                        else
132                        {
133                                onCancel(target);
134                        }
135                }
136        }
137
138        protected class LabelAjaxBehavior extends AjaxEventBehavior
139        {
140                private static final long serialVersionUID = 1L;
141
142                /**
143                 * Construct.
144                 * 
145                 * @param event
146                 */
147                public LabelAjaxBehavior(final String event)
148                {
149                        super(event);
150                }
151
152                @Override
153                protected void onEvent(final AjaxRequestTarget target)
154                {
155                        onEdit(target);
156                }
157
158                @Override
159                protected void updateAjaxAttributes(AjaxRequestAttributes attributes)
160                {
161                        super.updateAjaxAttributes(attributes);
162
163                        AjaxEditableLabel.this.updateLabelAjaxAttributes(attributes);
164                }
165        }
166
167        /**
168         * Gives a chance to the specializations to modify the Ajax attributes for the request when this
169         * component switches from an editor to a label.
170         * 
171         * @param attributes
172         *            The Ajax attributes to modify
173         */
174        protected void updateLabelAjaxAttributes(AjaxRequestAttributes attributes)
175        {
176        }
177
178        /**
179         * Gives a chance to the specializations to modify the Ajax attributes for the request when this
180         * component switches from a label to an editor.
181         * 
182         * @param attributes
183         *            The Ajax attributes to modify
184         */
185        protected void updateEditorAjaxAttributes(AjaxRequestAttributes attributes)
186        {
187        }
188
189        /**
190         * Constructor
191         * 
192         * @param id
193         */
194        public AjaxEditableLabel(final String id)
195        {
196                super(id);
197                setOutputMarkupId(true);
198        }
199
200        /**
201         * Constructor
202         * 
203         * @param id
204         * @param model
205         */
206        public AjaxEditableLabel(final String id, final IModel<T> model)
207        {
208                super(id, model);
209                setOutputMarkupId(true);
210        }
211
212        /**
213         * Adds a validator to this form component. A model must be available for this component before
214         * Validators can be added. Either add this Component to its parent (already having a Model), or
215         * provide one before this call via constructor {@link #AjaxEditableLabel(String,IModel)} or
216         * {@link #setDefaultModel(IModel)}.
217         * 
218         * @param validator
219         *            The validator
220         * @return This
221         */
222        public final AjaxEditableLabel<T> add(final IValidator<T> validator)
223        {
224                getEditor().add(validator);
225                return this;
226        }
227
228        /**
229         * The value will be made available to the validator property by means of ${label}. It does not
230         * have any specific meaning to FormComponent itself.
231         * 
232         * @param labelModel
233         * @return this for chaining
234         */
235        public final AjaxEditableLabel<T> setLabel(final IModel<String> labelModel)
236        {
237                getEditor().setLabel(labelModel);
238                return this;
239        }
240
241        @Override
242        public final AjaxEditableLabel<T> setDefaultModel(final IModel<?> model)
243        {
244                super.setDefaultModel(model);
245                getLabel().setDefaultModel(model);
246                getEditor().setDefaultModel(model);
247                return this;
248        }
249
250        /**
251         * Sets the required flag
252         * 
253         * @param required
254         * @return this for chaining
255         */
256        public final AjaxEditableLabel<T> setRequired(final boolean required)
257        {
258                getEditor().setRequired(required);
259                return this;
260        }
261
262        /**
263         * Sets the type that will be used when updating the model for this component. If no type is
264         * specified String type is assumed.
265         * 
266         * @param type
267         * @return this for chaining
268         */
269        public final AjaxEditableLabel<T> setType(final Class<?> type)
270        {
271                getEditor().setType(type);
272                return this;
273        }
274
275        /**
276         * Create a new form component instance to serve as editor.
277         * 
278         * @param parent
279         *            The parent component
280         * @param componentId
281         *            Id that should be used by the component
282         * @param model
283         *            The model
284         * @return The editor
285         */
286        protected FormComponent<T> newEditor(final MarkupContainer parent, final String componentId,
287                final IModel<T> model)
288        {
289                TextField<T> editor = new TextField<T>(componentId, model)
290                {
291                        private static final long serialVersionUID = 1L;
292
293                        @Override
294                        protected boolean shouldTrimInput()
295                        {
296                                return AjaxEditableLabel.this.shouldTrimInput();
297                        }
298
299                        /**
300                         * {@inheritDoc}
301                         */
302                        @Override
303                        public <C> IConverter<C> getConverter(final Class<C> type)
304                        {
305                                return AjaxEditableLabel.this.getConverter(type);
306                        }
307
308                        @Override
309                        protected void onModelChanged()
310                        {
311                                super.onModelChanged();
312                                AjaxEditableLabel.this.onModelChanged();
313                        }
314
315                        @Override
316                        protected void onModelChanging()
317                        {
318                                super.onModelChanging();
319                                AjaxEditableLabel.this.onModelChanging();
320                        }
321                };
322                editor.setOutputMarkupId(true);
323                editor.setVisible(false);
324                editor.add(new EditorAjaxBehavior()
325                {
326                        @Override
327                        protected void updateAjaxAttributes(AjaxRequestAttributes attributes)
328                        {
329                                super.updateAjaxAttributes(attributes);
330                                attributes.setEventNames("blur", "keyup", "keydown");
331
332                                // Note: preventDefault is handled selectively below
333                                attributes.setPreventDefault(false);
334
335                                // Note: escape can be detected on keyup, enter can be detected on keyup
336                                CharSequence precondition = "var kc=Wicket.Event.keyCode(attrs.event),"
337                                        + "evtType=attrs.event.type,"
338                                        + "ret=false;"
339                                        + "if (evtType==='blur' || (evtType==='keyup' && kc===27) || (evtType==='keydown' && kc===13)) {attrs.event.preventDefault(); ret = true;}"
340                                        + "return ret;";
341                                AjaxCallListener ajaxCallListener = new AjaxCallListener();
342                                ajaxCallListener.onPrecondition(precondition);
343
344                                CharSequence dynamicExtraParameters = "var result,"
345                                        + "evtType=attrs.event.type;"
346                                        + "if (evtType === 'keyup') { result = { 'save': false }; }"
347                                        + "else { result = { 'save': true }; }"
348                                        + "return result;";
349                                attributes.getDynamicExtraParameters().add(dynamicExtraParameters);
350
351                                attributes.getAjaxCallListeners().add(ajaxCallListener);
352
353                        }
354                });
355                return editor;
356        }
357
358        /**
359         * Determines whether or not the text field should trim its input prior to processing it. The
360         * default value is <code>true</code>
361         * 
362         * @return True if the input should be trimmed.
363         */
364        protected boolean shouldTrimInput()
365        {
366                return true;
367        }
368
369        /**
370         * Create a new form component instance to serve as label.
371         * 
372         * @param parent
373         *            The parent component
374         * @param componentId
375         *            Id that should be used by the component
376         * @param model
377         *            The model
378         * @return The editor
379         */
380        protected Component newLabel(final MarkupContainer parent, final String componentId,
381                final IModel<T> model)
382        {
383                Label label = new Label(componentId, model)
384                {
385                        private static final long serialVersionUID = 1L;
386
387                        @Override
388                        public <C> IConverter<C> getConverter(final Class<C> type)
389                        {
390                                return AjaxEditableLabel.this.getConverter(type);
391                        }
392
393                        @Override
394                        protected void onConfigure()
395                        {
396                                super.onConfigure();
397                                setEscapeModelStrings(AjaxEditableLabel.this.getEscapeModelStrings());
398                        }
399
400                        @Override
401                        public void onComponentTagBody(final MarkupStream markupStream,
402                                final ComponentTag openTag)
403                        {
404                                Object modelObject = getDefaultModelObject();
405                                if ((modelObject == null) || (modelObject instanceof String && ((String) modelObject).isEmpty()))
406                                {
407                                        String nullLabel = defaultNullLabel();
408                                        replaceComponentTagBody(markupStream, openTag,
409                                                getEscapeModelStrings() ? Strings.escapeMarkup(nullLabel) : nullLabel);
410                                }
411                                else
412                                {
413                                        super.onComponentTagBody(markupStream, openTag);
414                                }
415                        }
416                };
417                label.setOutputMarkupId(true);
418                label.add(new LabelAjaxBehavior(getLabelAjaxEvent()));
419                return label;
420        }
421
422        /**
423         * By default this returns "click", users can overwrite this on which event the label behavior
424         * should be triggered
425         * 
426         * @return The event name
427         */
428        protected String getLabelAjaxEvent()
429        {
430                return "click";
431        }
432
433
434        /**
435         * Gets the editor component.
436         * 
437         * @return The editor component
438         */
439        protected final FormComponent<T> getEditor()
440        {
441                if (editor == null)
442                {
443                        initLabelAndEditor(new WrapperModel());
444                }
445                return editor;
446        }
447
448        /**
449         * Gets the label component.
450         * 
451         * @return The label component
452         */
453        protected final Component getLabel()
454        {
455                if (label == null)
456                {
457                        initLabelAndEditor(new WrapperModel());
458                }
459                return label;
460        }
461
462        @Override
463        protected void onBeforeRender()
464        {
465                super.onBeforeRender();
466                // lazily add label and editor
467                if (editor == null)
468                {
469                        initLabelAndEditor(new WrapperModel());
470                }
471                // obsolete with WICKET-1919
472                // label.setEnabled(isEnabledInHierarchy());
473        }
474
475        /**
476         * Invoked when the label is in edit mode, and received a cancel event. Typically, nothing
477         * should be done here.
478         * 
479         * @param target
480         *            the ajax request target
481         */
482        protected void onCancel(final AjaxRequestTarget target)
483        {
484                label.setVisible(true);
485                editor.setVisible(false);
486                editor.clearInput();
487                target.add(AjaxEditableLabel.this);
488        }
489
490        /**
491         * Called when the label is clicked and the component is put in edit mode.
492         * 
493         * @param target
494         *            Ajax target
495         */
496        public void onEdit(final AjaxRequestTarget target)
497        {
498                label.setVisible(false);
499                editor.setVisible(true);
500                target.add(AjaxEditableLabel.this);
501                String selectScript = String.format(
502                        "(function(){var el = Wicket.$('%s'); if (el.select) el.select();})()",
503                        editor.getMarkupId());
504                target.appendJavaScript(selectScript);
505                target.focusComponent(editor);
506        }
507
508        /**
509         * Invoked when the label is in edit mode, received a new input, but that input didn't validate
510         * 
511         * @param target
512         *            the ajax request target
513         */
514        protected void onError(final AjaxRequestTarget target)
515        {
516                if (editor.hasErrorMessage())
517                {
518                        Serializable errorMessage = editor.getFeedbackMessages().first(FeedbackMessage.ERROR);
519                        target.appendJavaScript("window.status='" +
520                                JavaScriptUtils.escapeQuotes(errorMessage.toString()) + "';");
521                }
522                String selectAndFocusScript = String.format(
523                        "(function(){var el=Wicket.$('%s'); if (el.select) el.select(); el.focus();})()",
524                        editor.getMarkupId());
525                target.appendJavaScript(selectAndFocusScript);
526        }
527
528        /**
529         * Invoked when the editor was successfully updated. Use this method e.g. to persist the changed
530         * value. This implementation displays the label and clears any window status that might have
531         * been set in onError.
532         * 
533         * @param target
534         *            The ajax request target
535         */
536        protected void onSubmit(final AjaxRequestTarget target)
537        {
538                label.setVisible(true);
539                editor.setVisible(false);
540                target.add(AjaxEditableLabel.this);
541
542                target.appendJavaScript("window.status='';");
543        }
544
545        /**
546         * Lazy initialization of the label and editor components and set tempModel to null.
547         * 
548         * @param model
549         *            The model for the label and editor
550         */
551        private void initLabelAndEditor(final IModel<T> model)
552        {
553                editor = newEditor(this, "editor", model);
554                label = newLabel(this, "label", model);
555                add(label);
556                add(editor);
557        }
558
559        /**
560         * Model that accesses the parent model lazily. this is required since we eventually request the
561         * parents model before the component is added to the parent.
562         */
563        private class WrapperModel implements IModel<T>, IObjectClassAwareModel<T>
564        {
565                @Override
566                public T getObject()
567                {
568                        return getParentModel().getObject();
569                }
570
571                @Override
572                public void setObject(final T object)
573                {
574                        getParentModel().setObject(object);
575                }
576
577                @Override
578                public void detach()
579                {
580                        getParentModel().detach();
581                }
582
583                @Override
584                public Class<T> getObjectClass()
585                {
586                        if (getParentModel() instanceof IObjectClassAwareModel)
587                        {
588                                return ((IObjectClassAwareModel)getParentModel()).getObjectClass();
589                        }
590                        else
591                        {
592                                return null;
593                        }
594                }
595        }
596
597        /**
598         * @return Gets the parent model in case no explicit model was specified.
599         */
600        private IModel<T> getParentModel()
601        {
602                // the #getModel() call below will resolve and assign any inheritable
603                // model this component can use. Set that directly to the label and
604                // editor so that those components work like this enclosing panel
605                // does not exist (must have that e.g. with CompoundPropertyModels)
606                IModel<T> m = getModel();
607
608                // check that a model was found
609                if (m == null)
610                {
611                        Component parent = getParent();
612                        String msg = "No model found for this component, either pass one explicitly or "
613                                + "make sure an inheritable model is available.";
614                        if (parent == null)
615                        {
616                                msg += " This component is not added to a parent yet, so if this component "
617                                        + "is supposed to use the model of the parent (e.g. when it uses a "
618                                        + "compound property model), add it first before further configuring "
619                                        + "the component calling methods like e.g. setType and addValidator.";
620                        }
621                        throw new IllegalStateException(msg);
622                }
623                return m;
624        }
625
626        /**
627         * Override this to display a different value when the model object is null. Default is
628         * <code>...</code>
629         * 
630         * @return The string which should be displayed when the model object is null.
631         */
632        protected String defaultNullLabel()
633        {
634                return "...";
635        }
636
637        /**
638         * Dummy override to fix WICKET-1239
639         */
640        @Override
641        protected void onModelChanged()
642        {
643                super.onModelChanged();
644        }
645
646        /**
647         * Dummy override to fix WICKET-1239
648         */
649        @Override
650        protected void onModelChanging()
651        {
652                super.onModelChanging();
653        }
654}