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}