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}