@qooxdoo/framework
Version:
The JS Framework for Coders
531 lines (444 loc) • 14.7 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(value) {
super(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(value) {
value = super.setValue(value);
this.__autoSize();
return value;
},
/**
* Handles the roll for scrolling the <code>TextArea</code>.
*
* @param e {qx.event.type.Roll} roll event.
*/
_onRoll(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(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() {
if (this.isAutoSize()) {
var clone = this.__getAreaClone();
if (clone && this.getBounds()) {
// Remember original area height
this.__originalAreaHeight =
this.__originalAreaHeight || this._getAreaHeight();
var scrolledHeight = Math.round(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", () => {
this.__autoSize();
});
}
}
},
/**
* Get actual height of <code>TextArea</code>
*
* @return {Integer} Height of <code>TextArea</code>
*/
_getAreaHeight() {
return this.getInnerSize().height;
},
/**
* Set actual height of <code>TextArea</code>
*
* @param height {Integer} Desired height of <code>TextArea</code>
*/
_setAreaHeight(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() {
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() {
this.__areaClone = this.__areaClone || this.__createAreaClone();
return this.__areaClone;
},
/**
* Creates and prepares the area clone.
*
* @return {Element} Element
*/
__createAreaClone() {
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.useNode(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(clone) {
clone = clone.getDomElement();
if (clone) {
clone.scrollTop = 10000;
}
},
/*
---------------------------------------------------------------------------
FIELD API
---------------------------------------------------------------------------
*/
// overridden
_createInputElement() {
return new qx.html.Input("textarea", {
overflowX: "auto",
overflowY: "auto"
});
},
/*
---------------------------------------------------------------------------
APPLY ROUTINES
---------------------------------------------------------------------------
*/
// property apply
_applyWrap(value, old) {
this.getContentElement().setWrap(value);
if (this._placeholder) {
var whiteSpace = value ? "normal" : "nowrap";
this._placeholder.setStyle("whiteSpace", whiteSpace);
}
this.__autoSize();
},
// property apply
_applyMinimalLineHeight() {
qx.ui.core.queue.Layout.add(this);
},
// property apply
_applyAutoSize(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(value) {
super._applyDimension();
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() {
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() {
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() {
var hint = super._getContentHint();
// 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() {
this.setAutoSize(false);
if (this.__areaClone) {
this.__areaClone.dispose();
}
}
});