UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

541 lines (439 loc) 14.8 kB
/* ************************************************************************ 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(); } } });