@qooxdoo/framework
Version:
The JS Framework for Coders
541 lines (439 loc) • 14.8 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2004-2008 1&1 Internet AG, Germany, http://www.1und1.de
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Sebastian Werner (wpbasti)
* Andreas Ecker (ecker)
* Jonathan Weiß (jonathan_rass)
* Tristan Koch (tristankoch)
************************************************************************ */
/**
* The TextField is a multi-line text input field.
*/
qx.Class.define("qx.ui.form.TextArea",
{
extend : qx.ui.form.AbstractField,
/*
*****************************************************************************
CONSTRUCTOR
*****************************************************************************
*/
/**
* @param value {String?""} The text area's initial value
*/
construct : function(value)
{
this.base(arguments, value);
this.initWrap();
this.addListener("roll", this._onRoll, this);
this.addListener("resize", this._onResize, this);
},
/*
*****************************************************************************
PROPERTIES
*****************************************************************************
*/
properties :
{
/** Controls whether text wrap is activated or not. */
wrap :
{
check : "Boolean",
init : true,
apply : "_applyWrap"
},
// overridden
appearance :
{
refine : true,
init : "textarea"
},
/** Factor for scrolling the <code>TextArea</code> with the mouse wheel. */
singleStep :
{
check : "Integer",
init : 20
},
/** Minimal line height. On default this is set to four lines. */
minimalLineHeight :
{
check : "Integer",
apply : "_applyMinimalLineHeight",
init : 4
},
/**
* Whether the <code>TextArea</code> should automatically adjust to
* the height of the content.
*
* To set the initial height, modify {@link #minHeight}. If you wish
* to set a minHeight below four lines of text, also set
* {@link #minimalLineHeight}. In order to limit growing to a certain
* height, set {@link #maxHeight} respectively. Please note that
* autoSize is ignored when the {@link #height} property is in use.
*/
autoSize :
{
check : "Boolean",
apply : "_applyAutoSize",
init : false
}
},
/*
*****************************************************************************
MEMBERS
*****************************************************************************
*/
members :
{
__areaClone : null,
__areaHeight : null,
__originalAreaHeight : null,
// overridden
setValue : function(value)
{
value = this.base(arguments, value);
this.__autoSize();
return value;
},
/**
* Handles the roll for scrolling the <code>TextArea</code>.
*
* @param e {qx.event.type.Roll} roll event.
*/
_onRoll : function(e) {
// only wheel
if (e.getPointerType() != "wheel") {
return;
}
var contentElement = this.getContentElement();
var scrollY = contentElement.getScrollY();
contentElement.scrollToY(scrollY + (e.getDelta().y / 30) * this.getSingleStep());
var newScrollY = contentElement.getScrollY();
if (newScrollY != scrollY) {
e.stop();
}
},
/**
* When the element resizes we throw away the clone and trigger autosize again, otherwise the clone would have
* another width and the autosize calculation would be faulty.
*
* @param e {qx.event.type.Data} resize event.
*/
_onResize : function(e) {
if (this.__areaClone) {
this.__areaClone.dispose();
this.__areaClone = null;
this.__autoSize();
}
},
/*
---------------------------------------------------------------------------
AUTO SIZE
---------------------------------------------------------------------------
*/
/**
* Adjust height of <code>TextArea</code> so that content fits without scroll bar.
*
*/
__autoSize: function() {
if (this.isAutoSize()) {
var clone = this.__getAreaClone();
if (clone && this.getBounds()) {
// Remember original area height
this.__originalAreaHeight = this.__originalAreaHeight || this._getAreaHeight();
var scrolledHeight = this._getScrolledAreaHeight();
// Show scroll-bar when above maxHeight, if defined
if (this.getMaxHeight()) {
var insets = this.getInsets();
var innerMaxHeight = -insets.top + this.getMaxHeight() - insets.bottom;
if (scrolledHeight > innerMaxHeight) {
this.getContentElement().setStyle("overflowY", "auto");
} else {
this.getContentElement().setStyle("overflowY", "hidden");
}
}
// Never shrink below original area height
var desiredHeight = Math.max(scrolledHeight, this.__originalAreaHeight);
// Set new height
this._setAreaHeight(desiredHeight);
// On init, the clone is not yet present. Try again on appear.
} else {
this.getContentElement().addListenerOnce("appear", function() {
this.__autoSize();
}, this);
}
}
},
/**
* Get actual height of <code>TextArea</code>
*
* @return {Integer} Height of <code>TextArea</code>
*/
_getAreaHeight: function() {
return this.getInnerSize().height;
},
/**
* Set actual height of <code>TextArea</code>
*
* @param height {Integer} Desired height of <code>TextArea</code>
*/
_setAreaHeight: function(height) {
if (this._getAreaHeight() !== height) {
this.__areaHeight = height;
qx.ui.core.queue.Layout.add(this);
// Apply height directly. This works-around a visual glitch in WebKit
// browsers where a line-break causes the text to be moved upwards
// for one line. Since this change appears instantly whereas the queue
// is computed later, a flicker is visible.
qx.ui.core.queue.Manager.flush();
this.__forceRewrap();
}
},
/**
* Get scrolled area height. Equals the total height of the <code>TextArea</code>,
* as if no scroll-bar was visible.
*
* @return {Integer} Height of scrolled area
*/
_getScrolledAreaHeight: function() {
var clone = this.__getAreaClone();
var cloneDom = clone.getDomElement();
if (cloneDom) {
// Clone created but not yet in DOM. Try again.
if (!cloneDom.parentNode) {
qx.html.Element.flush();
return this._getScrolledAreaHeight();
}
// In WebKit and IE8, "wrap" must have been "soft" on DOM level before setting
// "off" can disable wrapping. To fix, make sure wrap is toggled.
// Otherwise, the height of an auto-size text area with wrapping
// disabled initially is incorrectly computed as if wrapping was enabled.
if (qx.core.Environment.get("engine.name") === "webkit" ||
(qx.core.Environment.get("engine.name") == "mshtml")) {
clone.setWrap(!this.getWrap(), true);
}
clone.setWrap(this.getWrap(), true);
// Webkit needs overflow "hidden" in order to correctly compute height
if (qx.core.Environment.get("engine.name") === "webkit" ||
(qx.core.Environment.get("engine.name") == "mshtml")) {
cloneDom.style.overflow = "hidden";
}
// IE >= 8 needs overflow "visible" in order to correctly compute height
if (qx.core.Environment.get("engine.name") == "mshtml" &&
qx.core.Environment.get("browser.documentmode") >= 8) {
cloneDom.style.overflow = "visible";
cloneDom.style.overflowX = "hidden";
}
// Update value
clone.setValue(this.getValue() || "");
// Force IE > 8 to update size measurements
if (qx.core.Environment.get("engine.name") == "mshtml") {
cloneDom.style.height = "auto";
qx.html.Element.flush();
cloneDom.style.height = "0";
}
// Recompute
this.__scrollCloneToBottom(clone);
if (qx.core.Environment.get("engine.name") == "mshtml" &&
qx.core.Environment.get("browser.documentmode") == 8) {
// Flush required for scrollTop to return correct value
// when initial value should be taken into consideration
if (!cloneDom.scrollTop) {
qx.html.Element.flush();
}
}
return cloneDom.scrollTop;
}
},
/**
* Returns the area clone.
*
* @return {Element|null} DOM Element or <code>null</code> if there is no
* original element
*/
__getAreaClone: function() {
this.__areaClone = this.__areaClone || this.__createAreaClone();
return this.__areaClone;
},
/**
* Creates and prepares the area clone.
*
* @return {Element} Element
*/
__createAreaClone: function() {
var orig,
clone,
cloneDom,
cloneHtml;
orig = this.getContentElement();
// An existing DOM element is required
if (!orig.getDomElement()) {
return null;
}
// Create DOM clone
cloneDom = qx.bom.Element.clone(orig.getDomElement());
// Convert to qx.html Element
cloneHtml = new qx.html.Input("textarea");
cloneHtml.useElement(cloneDom);
clone = cloneHtml;
// Push out of view
// Zero height (i.e. scrolled area equals height)
clone.setStyles({
position: "absolute",
top: 0,
left: "-9999px",
height: 0,
overflow: "hidden"
}, true);
// Fix attributes
clone.removeAttribute('id');
clone.removeAttribute('name');
clone.setAttribute("tabIndex", "-1");
// Copy value
clone.setValue(orig.getValue() || "");
// Attach to DOM
clone.insertBefore(orig);
// Make sure scrollTop is actual height
this.__scrollCloneToBottom(clone);
return clone;
},
/**
* Scroll <code>TextArea</code> to bottom. That way, scrollTop reflects the height
* of the <code>TextArea</code>.
*
* @param clone {Element} The <code>TextArea</code> to scroll
*/
__scrollCloneToBottom: function(clone) {
clone = clone.getDomElement();
if (clone) {
clone.scrollTop = 10000;
}
},
/*
---------------------------------------------------------------------------
FIELD API
---------------------------------------------------------------------------
*/
// overridden
_createInputElement : function()
{
return new qx.html.Input("textarea", {
overflowX: "auto",
overflowY: "auto"
});
},
/*
---------------------------------------------------------------------------
APPLY ROUTINES
---------------------------------------------------------------------------
*/
// property apply
_applyWrap : function(value, old) {
this.getContentElement().setWrap(value);
if (this._placeholder) {
var whiteSpace = value ? "normal" : "nowrap";
this._placeholder.setStyle("whiteSpace", whiteSpace);
}
this.__autoSize();
},
// property apply
_applyMinimalLineHeight : function() {
qx.ui.core.queue.Layout.add(this);
},
// property apply
_applyAutoSize: function(value, old) {
if (qx.core.Environment.get("qx.debug")) {
this.__warnAutoSizeAndHeight();
}
if (value) {
this.__autoSize();
this.addListener("input", this.__autoSize, this);
// This is done asynchronously on purpose. The style given would
// otherwise be overridden by the DOM changes queued in the
// property apply for wrap. See [BUG #4493] for more details.
if (!this.getBounds()) {
this.addListenerOnce("appear", function() {
this.getContentElement().setStyle("overflowY", "hidden");
});
} else {
this.getContentElement().setStyle("overflowY", "hidden");
}
} else {
this.removeListener("input", this.__autoSize);
this.getContentElement().setStyle("overflowY", "auto");
}
},
// property apply
_applyDimension : function(value) {
this.base(arguments);
if (qx.core.Environment.get("qx.debug")) {
this.__warnAutoSizeAndHeight();
}
if (value === this.getMaxHeight()) {
this.__autoSize();
}
},
/**
* Force rewrapping of text.
*
* The distribution of characters depends on the space available.
* Unfortunately, browsers do not reliably (or not at all) rewrap text when
* the size of the text area changes.
*
* This method is called on change of the area's size.
*/
__forceRewrap : function() {
var content = this.getContentElement();
var element = content.getDomElement();
// Temporarily increase width
var width = content.getStyle("width");
content.setStyle("width", parseInt(width, 10) + 1000 + "px", true);
// Force browser to render
if (element) {
qx.bom.element.Dimension.getWidth(element);
}
// Restore width
content.setStyle("width", width, true);
},
/**
* Warn when both autoSize and height property are set.
*
*/
__warnAutoSizeAndHeight: function() {
if (this.isAutoSize() && this.getHeight()) {
this.warn("autoSize is ignored when the height property is set. " +
"If you want to set an initial height, use the minHeight " +
"property instead.");
}
},
/*
---------------------------------------------------------------------------
LAYOUT
---------------------------------------------------------------------------
*/
// overridden
_getContentHint : function()
{
var hint = this.base(arguments);
// lines of text
hint.height = hint.height * this.getMinimalLineHeight();
// 20 character wide
hint.width = this._getTextSize().width * 20;
if (this.isAutoSize()) {
hint.height = this.__areaHeight || hint.height;
}
return hint;
}
},
destruct : function() {
this.setAutoSize(false);
if (this.__areaClone) {
this.__areaClone.dispose();
}
}
});