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