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