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}