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}