001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.gwt.client.ui.input; 029 030import org.opencms.gwt.client.I_CmsHasInit; 031import org.opencms.gwt.client.I_CmsHasResizeOnShow; 032import org.opencms.gwt.client.ui.CmsScrollPanel; 033import org.opencms.gwt.client.ui.I_CmsAutoHider; 034import org.opencms.gwt.client.ui.css.I_CmsInputLayoutBundle; 035import org.opencms.gwt.client.ui.css.I_CmsLayoutBundle; 036import org.opencms.gwt.client.ui.input.form.CmsWidgetFactoryRegistry; 037import org.opencms.gwt.client.ui.input.form.I_CmsFormWidgetFactory; 038import org.opencms.gwt.client.util.CmsDomUtil; 039import org.opencms.util.CmsStringUtil; 040 041import java.util.Map; 042 043import com.google.common.base.Objects; 044import com.google.common.base.Optional; 045import com.google.gwt.core.client.GWT; 046import com.google.gwt.core.client.Scheduler; 047import com.google.gwt.core.client.Scheduler.ScheduledCommand; 048import com.google.gwt.dom.client.Element; 049import com.google.gwt.event.dom.client.BlurEvent; 050import com.google.gwt.event.dom.client.BlurHandler; 051import com.google.gwt.event.dom.client.ClickEvent; 052import com.google.gwt.event.dom.client.ClickHandler; 053import com.google.gwt.event.dom.client.FocusEvent; 054import com.google.gwt.event.dom.client.FocusHandler; 055import com.google.gwt.event.dom.client.HasFocusHandlers; 056import com.google.gwt.event.dom.client.KeyUpEvent; 057import com.google.gwt.event.dom.client.KeyUpHandler; 058import com.google.gwt.event.logical.shared.HasResizeHandlers; 059import com.google.gwt.event.logical.shared.HasValueChangeHandlers; 060import com.google.gwt.event.logical.shared.ResizeHandler; 061import com.google.gwt.event.logical.shared.ValueChangeEvent; 062import com.google.gwt.event.logical.shared.ValueChangeHandler; 063import com.google.gwt.event.shared.HandlerRegistration; 064import com.google.gwt.user.client.DOM; 065import com.google.gwt.user.client.Timer; 066import com.google.gwt.user.client.ui.Composite; 067import com.google.gwt.user.client.ui.FlowPanel; 068import com.google.gwt.user.client.ui.Panel; 069import com.google.gwt.user.client.ui.SimplePanel; 070import com.google.gwt.user.client.ui.TextArea; 071 072/** 073 * Basic text area widget for forms.<p> 074 * 075 * @since 8.0.0 076 * 077 */ 078public class CmsTextArea extends Composite 079implements I_CmsFormWidget, I_CmsHasInit, HasValueChangeHandlers<String>, HasResizeHandlers, HasFocusHandlers, 080I_CmsHasResizeOnShow, I_CmsHasGhostValue { 081 082 /** The widget type identifier for this widget. */ 083 private static final String WIDGET_TYPE = "textarea"; 084 085 /** The default rows set. */ 086 int m_defaultRows; 087 088 /** The fade panel. */ 089 Panel m_fadePanel = new SimplePanel(); 090 091 /** The root panel containing the other components of this widget. */ 092 Panel m_panel = new FlowPanel(); 093 094 /** The internal text area widget used by this widget. */ 095 TextArea m_textArea = new TextArea(); 096 097 /** The container for the text area. */ 098 CmsScrollPanel m_textAreaContainer = GWT.create(CmsScrollPanel.class); 099 100 /** Overlay to disable the text area. */ 101 private Element m_disabledOverlay; 102 103 /** The error display for this widget. */ 104 private CmsErrorWidget m_error = new CmsErrorWidget(); 105 106 /** True if we are currently focused. */ 107 private boolean m_focus; 108 109 /** The 'ghost value' to display when no value is set. */ 110 private String m_ghostValue; 111 112 /** The real, logical value which might be different from the value displayed in the internal textarea. */ 113 private String m_realValue = ""; 114 115 /** A timer to schedule the widget size recalculation. */ 116 private Timer m_updateSizeTimer; 117 118 /** 119 * Text area widgets for ADE forms.<p> 120 */ 121 public CmsTextArea() { 122 123 super(); 124 initWidget(m_panel); 125 m_panel.setStyleName(I_CmsInputLayoutBundle.INSTANCE.inputCss().textArea()); 126 m_textAreaContainer.addStyleName(I_CmsInputLayoutBundle.INSTANCE.inputCss().textAreaBoxPanel()); 127 m_textArea.setStyleName(I_CmsInputLayoutBundle.INSTANCE.inputCss().textAreaBox()); 128 m_fadePanel.addStyleName(I_CmsInputLayoutBundle.INSTANCE.inputCss().fader()); 129 m_panel.add(m_textAreaContainer); 130 m_textAreaContainer.setResizable(true); 131 m_textAreaContainer.add(m_textArea); 132 m_fadePanel.addDomHandler(new ClickHandler() { 133 134 public void onClick(ClickEvent event) { 135 136 m_textArea.setFocus(true); 137 } 138 }, ClickEvent.getType()); 139 140 m_textArea.addKeyUpHandler(new KeyUpHandler() { 141 142 @SuppressWarnings("synthetic-access") 143 public void onKeyUp(KeyUpEvent event) { 144 145 scheduleResize(); 146 actionChangeTextareaValue(m_textArea.getValue()); 147 } 148 149 }); 150 151 m_textArea.addValueChangeHandler(new ValueChangeHandler<String>() { 152 153 @SuppressWarnings("synthetic-access") 154 public void onValueChange(ValueChangeEvent<String> event) { 155 156 actionChangeTextareaValue(event.getValue()); 157 } 158 }); 159 160 m_panel.add(m_error); 161 m_textAreaContainer.addStyleName(I_CmsLayoutBundle.INSTANCE.generalCss().cornerAll()); 162 163 m_textArea.addFocusHandler(new FocusHandler() { 164 165 @SuppressWarnings("synthetic-access") 166 public void onFocus(FocusEvent event) { 167 168 m_panel.remove(m_fadePanel); 169 CmsDomUtil.fireFocusEvent(CmsTextArea.this); 170 m_focus = true; 171 updateGhostStyle(); 172 } 173 }); 174 m_textArea.addBlurHandler(new BlurHandler() { 175 176 @SuppressWarnings("synthetic-access") 177 public void onBlur(BlurEvent event) { 178 179 showFadePanelIfNeeded(); 180 m_textAreaContainer.scrollToTop(); 181 m_focus = false; 182 updateGhostStyle(); 183 } 184 }); 185 } 186 187 /** 188 * Initializes this class.<p> 189 */ 190 public static void initClass() { 191 192 // registers a factory for creating new instances of this widget 193 CmsWidgetFactoryRegistry.instance().registerFactory(WIDGET_TYPE, new I_CmsFormWidgetFactory() { 194 195 public I_CmsFormWidget createWidget(Map<String, String> widgetParams, Optional<String> defaultValue) { 196 197 return new CmsTextArea(); 198 } 199 }); 200 } 201 202 /** 203 * @see com.google.gwt.event.dom.client.HasFocusHandlers#addFocusHandler(com.google.gwt.event.dom.client.FocusHandler) 204 */ 205 public HandlerRegistration addFocusHandler(FocusHandler handler) { 206 207 return addDomHandler(handler, FocusEvent.getType()); 208 } 209 210 /** 211 * @see com.google.gwt.event.logical.shared.HasResizeHandlers#addResizeHandler(com.google.gwt.event.logical.shared.ResizeHandler) 212 */ 213 public HandlerRegistration addResizeHandler(ResizeHandler handler) { 214 215 return m_textAreaContainer.addResizeHandler(handler); 216 } 217 218 /** 219 * @see com.google.gwt.event.logical.shared.HasValueChangeHandlers#addValueChangeHandler(com.google.gwt.event.logical.shared.ValueChangeHandler) 220 */ 221 public HandlerRegistration addValueChangeHandler(ValueChangeHandler<String> handler) { 222 223 return addHandler(handler, ValueChangeEvent.getType()); 224 } 225 226 /** 227 * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#getApparentValue() 228 */ 229 public String getApparentValue() { 230 231 return getFormValueAsString(); 232 } 233 234 /** 235 * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#getFieldType() 236 */ 237 public FieldType getFieldType() { 238 239 return I_CmsFormWidget.FieldType.STRING; 240 } 241 242 /** 243 * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#getFormValue() 244 */ 245 public Object getFormValue() { 246 247 return nullToEmpty(m_realValue); 248 } 249 250 /** 251 * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#getFormValueAsString() 252 */ 253 public String getFormValueAsString() { 254 255 return (String)getFormValue(); 256 } 257 258 /** 259 * Returns the text contained in the text area.<p> 260 * 261 * @return the text in the text area 262 */ 263 public String getText() { 264 265 return m_textArea.getText(); 266 } 267 268 /** 269 * Returns the textarea of this widget.<p> 270 * 271 * @return the textarea 272 */ 273 public TextArea getTextArea() { 274 275 return m_textArea; 276 } 277 278 /** 279 * Returns the text area container of this widget.<p> 280 * 281 * @return the text area container 282 */ 283 public CmsScrollPanel getTextAreaContainer() { 284 285 return m_textAreaContainer; 286 } 287 288 /** 289 * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#isEnabled() 290 */ 291 public boolean isEnabled() { 292 293 return m_textArea.isEnabled(); 294 } 295 296 /** 297 * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#reset() 298 */ 299 public void reset() { 300 301 m_textArea.setText(""); 302 } 303 304 /** 305 * @see org.opencms.gwt.client.I_CmsHasResizeOnShow#resizeOnShow() 306 */ 307 public void resizeOnShow() { 308 309 m_textAreaContainer.onResizeDescendant(); 310 updateContentSize(); 311 showFadePanelIfNeeded(); 312 } 313 314 /** 315 * Selects all content.<p> 316 */ 317 public void selectAll() { 318 319 m_textArea.selectAll(); 320 } 321 322 /** 323 * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#setAutoHideParent(org.opencms.gwt.client.ui.I_CmsAutoHider) 324 */ 325 public void setAutoHideParent(I_CmsAutoHider autoHideParent) { 326 327 // nothing to do 328 } 329 330 /** 331 * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#setEnabled(boolean) 332 */ 333 public void setEnabled(boolean enabled) { 334 335 m_textArea.setEnabled(enabled); 336 // hide / show resize handle 337 m_textAreaContainer.setResizable(enabled); 338 if (enabled) { 339 if (m_disabledOverlay != null) { 340 m_disabledOverlay.removeFromParent(); 341 m_disabledOverlay = null; 342 } 343 } else { 344 if (m_disabledOverlay == null) { 345 m_disabledOverlay = DOM.createDiv(); 346 m_disabledOverlay.setClassName(I_CmsInputLayoutBundle.INSTANCE.inputCss().disableTextArea()); 347 m_panel.getElement().appendChild(m_disabledOverlay); 348 } 349 } 350 } 351 352 /** 353 * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#setErrorMessage(java.lang.String) 354 */ 355 public void setErrorMessage(String errorMessage) { 356 357 m_error.setText(errorMessage); 358 } 359 360 /** 361 * Sets or removes focus.<p> 362 * 363 * @param b true if the text area should be focused 364 */ 365 public void setFocus(boolean b) { 366 367 m_textArea.setFocus(b); 368 369 } 370 371 /** 372 * Sets the value of the widget.<p> 373 * 374 * @param value the new value 375 */ 376 public void setFormValue(Object value) { 377 378 setRealValue((String)value); 379 updateGhostStyle(); 380 381 } 382 383 /** 384 * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#setFormValueAsString(java.lang.String) 385 */ 386 public void setFormValueAsString(String newValue) { 387 388 setFormValue(newValue); 389 } 390 391 /** 392 * Not used.<p> 393 * 394 * This widget automatically decides whether it's in ghost mode depending on the real value and ghost value set.<p> 395 * 396 * @param ghostMode not used 397 */ 398 public void setGhostMode(boolean ghostMode) { 399 // do nothing 400 401 } 402 403 /** 404 * Enables or disables the 'ghost mode' style.<p> 405 * 406 * @param enabled true if 'ghost mode' style should be enabled 407 */ 408 public void setGhostStyleEnabled(boolean enabled) { 409 410 String styleName = I_CmsInputLayoutBundle.INSTANCE.inputCss().textAreaGhostMode(); 411 if (enabled) { 412 addStyleName(styleName); 413 } else { 414 removeStyleName(styleName); 415 } 416 } 417 418 /** 419 * @see org.opencms.gwt.client.ui.input.I_CmsHasGhostValue#setGhostValue(java.lang.String, boolean) 420 */ 421 public void setGhostValue(String value, boolean ghostMode) { 422 423 m_ghostValue = value; 424 updateGhostStyle(); 425 } 426 427 /** 428 * Sets the name of the input field.<p> 429 * 430 * @param name of the input field 431 * 432 * */ 433 public void setName(String name) { 434 435 m_textArea.setName(name); 436 437 } 438 439 /** 440 * Sets the text area to use a proportional font.<p> 441 * 442 * @param proportional <code>true</code> to use a proportional font 443 */ 444 public void setProportionalStyle(boolean proportional) { 445 446 if (proportional) { 447 m_panel.addStyleName(I_CmsInputLayoutBundle.INSTANCE.inputCss().textAreaProportional()); 448 } else { 449 m_panel.removeStyleName(I_CmsInputLayoutBundle.INSTANCE.inputCss().textAreaProportional()); 450 } 451 } 452 453 /** 454 * Sets the height of this textarea.<p> 455 * 456 * @param rows the value of rows should be shown 457 */ 458 public void setRows(int rows) { 459 460 m_defaultRows = rows; 461 double height_scroll = (rows * 17.95) + 8; 462 m_textArea.setVisibleLines(rows); 463 m_textAreaContainer.setHeight(height_scroll + "px"); 464 m_textAreaContainer.setDefaultHeight(height_scroll); 465 m_textAreaContainer.onResizeDescendant(); 466 } 467 468 /** 469 * Sets the height of this textarea. Especial for the image Gallery.<p> 470 * 471 * @param rows the value of rows should be shown 472 */ 473 public void setRowsGallery(int rows) { 474 475 m_defaultRows = rows; 476 double height_scroll = (rows * 17.95) + 8 + 5; 477 m_textArea.setVisibleLines(rows); 478 m_textAreaContainer.setHeight(height_scroll + "px"); 479 m_textAreaContainer.setDefaultHeight(height_scroll); 480 m_textAreaContainer.onResizeDescendant(); 481 } 482 483 /** 484 * Sets the text in the text area.<p> 485 * 486 * @param text the new text 487 */ 488 public void setText(String text) { 489 490 m_textArea.setText(text); 491 } 492 493 /** 494 * @see com.google.gwt.user.client.ui.Composite#onAttach() 495 */ 496 @Override 497 protected void onAttach() { 498 499 super.onAttach(); 500 Scheduler.get().scheduleDeferred(new ScheduledCommand() { 501 502 public void execute() { 503 504 resizeOnShow(); 505 } 506 }); 507 } 508 509 /** 510 * Schedules resizing the widget.<p> 511 */ 512 protected void scheduleResize() { 513 514 if (m_updateSizeTimer != null) { 515 m_updateSizeTimer.cancel(); 516 } 517 m_updateSizeTimer = new Timer() { 518 519 @Override 520 public void run() { 521 522 updateContentSize(); 523 } 524 }; 525 m_updateSizeTimer.schedule(300); 526 } 527 528 /** 529 * Shows the fade panel if the text area content exceeds the visible area.<p> 530 */ 531 protected void showFadePanelIfNeeded() { 532 533 if (m_defaultRows < m_textArea.getVisibleLines()) { 534 m_panel.add(m_fadePanel); 535 } 536 } 537 538 /** 539 * Updates the text area height according to the current text content.<p> 540 */ 541 protected void updateContentSize() { 542 543 // The scrolling should be done by the CmsScrollPanel and not by the text area, 544 // so we try to make the text area itself as big as its content here. 545 546 int offsetHeight = m_textArea.getOffsetHeight(); 547 int origRows = m_textArea.getVisibleLines(); 548 // sanity check: don't do anything, if the measured height doesn't make any sense, e.g. if element is not attached 549 if (offsetHeight > 5) { 550 int scrollPosition = m_textAreaContainer.getVerticalScrollPosition(); 551 Element textareaElem = m_textArea.getElement(); 552 int rows = Math.max(0, m_defaultRows - 1); // we add 1 to it later, and the sum should be at least 1 and at least the default row number 553 int scrollHeight; 554 int prevOffsetHeight = -1; 555 do { 556 rows += 1; 557 m_textArea.setVisibleLines(rows); 558 scrollHeight = textareaElem.getScrollHeight(); 559 offsetHeight = textareaElem.getOffsetHeight(); 560 if (offsetHeight <= prevOffsetHeight) { 561 // Increasing the number of rows should normally increase the offset height. 562 // If it doesn't, e.g. because of CSS rules limiting the text area height, there is no point in continuing with the loop 563 break; 564 } 565 prevOffsetHeight = offsetHeight; 566 } while (offsetHeight < scrollHeight); 567 m_textAreaContainer.setVerticalScrollPosition(scrollPosition); 568 if (origRows != rows) { 569 m_textAreaContainer.onResizeDescendant(); 570 } 571 } 572 } 573 574 /** 575 * This method is called when the value of the internal textarea changes.<p> 576 * 577 * @param value the textarea value 578 */ 579 private void actionChangeTextareaValue(String value) { 580 581 if (m_focus) { 582 setRealValue(value); 583 updateGhostStyle(); 584 } 585 } 586 587 /** 588 * Converts null to an empty string, leaves other strings unchanged.<p> 589 * 590 * @param s the input string 591 * @return the input string if it was not null, otherwise the empty string 592 */ 593 private String nullToEmpty(String s) { 594 595 if (s == null) { 596 return ""; 597 } 598 return s; 599 } 600 601 /** 602 * Sets the 'real' (logical) value.<p> 603 * 604 * @param value the real value 605 */ 606 private void setRealValue(String value) { 607 608 if (!Objects.equal(value, m_realValue)) { 609 m_realValue = value; 610 ValueChangeEvent.fire(this, value); 611 } 612 } 613 614 /** 615 * Updates the styling and content of the internal text area based on the real value, the ghost value, and whether 616 * it has focus. 617 */ 618 private void updateGhostStyle() { 619 620 if (CmsStringUtil.isEmpty(m_realValue)) { 621 if (CmsStringUtil.isEmpty(m_ghostValue)) { 622 updateTextArea(m_realValue); 623 return; 624 } 625 if (!m_focus) { 626 setGhostStyleEnabled(true); 627 updateTextArea(m_ghostValue); 628 } else { 629 // don't show ghost mode while focused 630 setGhostStyleEnabled(false); 631 } 632 } else { 633 setGhostStyleEnabled(false); 634 updateTextArea(m_realValue); 635 } 636 } 637 638 /** 639 * Updates the content of the internal textarea.<p> 640 * 641 * @param value the new content 642 */ 643 private void updateTextArea(String value) { 644 645 String oldValue = m_textArea.getValue(); 646 if (!oldValue.equals(value)) { 647 int l1 = oldValue.split("\n").length; 648 int l2 = value.split("\n").length; 649 m_textArea.setValue(value); 650 if (l1 != l2) { 651 scheduleResize(); 652 } 653 } 654 } 655}