UNPKG

@openui5/sap.m

Version:

OpenUI5 UI Library sap.m

688 lines (578 loc) 21.6 kB
/*! * OpenUI5 * (c) Copyright 2026 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ // Provides control sap.m.TextArea. sap.ui.define([ './InputBase', './Text', "sap/ui/core/Lib", 'sap/ui/core/ResizeHandler', './library', 'sap/ui/core/library', 'sap/ui/events/KeyCodes', 'sap/ui/Device', "sap/base/security/encodeXML", './TextAreaRenderer', "sap/ui/core/Theming", "sap/ui/thirdparty/jquery" ], function( InputBase, Text, Lib, ResizeHandler, library, coreLibrary, KeyCodes, Device, encodeXML, TextAreaRenderer, Theming, jQuery ) { "use strict"; // shortcut for sap.ui.core.Wrapping var Wrapping = coreLibrary.Wrapping; /** * Constructor for a new TextArea. * * @param {string} [sId] ID for the new control, generated automatically if no ID is given * @param {object} [mSettings] Initial settings for the new control * * @class * A control that is used for multi-line input of text. * <h3>Overview</h3> * The text area is used to enter multiple lines of text. When empty, it can hold a placeholder similar to an {@link sap.m.Input input}. * You can define the height and width of the text area and also determine specific behavior when handling long texts. * <h3>Structure</h3> * Parameters that determine the size: * <ul> * <li><code>rows</code> - Number of visible text lines (overruled by <code>height</code>, if both are set)</li> * <li><code>cols</code> - Number of visible characters per line line (overruled by <code>width</code>, if both are set)</li> * <li><code>height</code> - Height of the control</li> * <li><code>width</code> - Width of the control</li> * </ul> * Parameters that determine the behavior: * <ul> * <li><code>growing</code> - The text area adjusts its size based on the content</li> * <li><code>growingMaxLines</code> - Threshold for the <code>growing</code> property (shouldn't exceed the screen size)</li> * <li><code>maxLength</code> - Maximum number of characters that can be entered in a text area</li> * <li><code>wrapping</code> - The way the entered text is wrapped by the control</li> * <li><code>showExceededText</code> - Determines how text beyond the <code>maxLength</code> length is handled</li> * </ul> * <h3>Usage</h3> * <h4>When to use</h4> * <ul> * <li>You want to enter multiple lines of text.</li> * <li>Always provide labels for a text area.</li> * <li>A placeholder does not substitute a label.</li> * </ul> * <h3>Responsive Behavior</h3> * <ul> * <li>On smaller screens, you can scroll down the text area to see the entire text. To indicate that the text continues, the control shows only half of the last line.</li> * <li>If you have a growing text area, have in mind that its maximum height should not exceed the height of the screen. If that is the case, the screen height is used instead.</li> * <li>If <code>showExceededText</code> is set to TRUE and you paste a longer text, all characters beyond the <code>maxLength</code> limit are automatically selected.</li> * <li>If <code>showExceededText</code> is set to TRUE, the control will display a counter for the remaining characters. * * @extends sap.m.InputBase * * @author SAP SE * @version 1.146.0 * * @constructor * @public * @since 1.9.0 * @alias sap.m.TextArea * @see {@link fiori:https://experience.sap.com/fiori-design-web/text-area/ Text Area} */ var TextArea = InputBase.extend("sap.m.TextArea", /** @lends sap.m.TextArea.prototype */ { metadata : { library : "sap.m", designtime: "sap/m/designtime/TextArea.designtime", properties : { /** * Defines the number of visible text lines for the control. * <b>Note:</b> The <code>height</code> property wins over the <code>rows</code> property, if both are set. */ rows : {type : "int", group : "Appearance", defaultValue : 2}, /** * Defines the visible width of the control, in average character widths. * <b>Note:</b> The <code>width</code> property wins over the <code>cols</code> property, if both are set. */ cols : {type : "int", group : "Appearance", defaultValue : 20}, /** * Defines the height of the control. */ height : {type : "sap.ui.core.CSSSize", group : "Appearance", defaultValue : null}, /** * Defines the maximum number of characters that the <code>value</code> can be. */ maxLength : {type : "int", group : "Behavior", defaultValue : 0}, /** * Determines whether the characters, exceeding the maximum allowed character count, are visible in the input field. * * If set to <code>false</code> the user is not allowed to enter more characters than what is set in the <code>maxLength</code> property. * If set to <code>true</code> the characters exceeding the <code>maxLength</code> value are selected on paste and the counter below * the input field displays their number. * @since 1.48 */ showExceededText: {type: "boolean", group: "Behavior", defaultValue: false}, /** * Indicates how the control wraps the text, e.g. <code>Soft</code>, <code>Hard</code>, <code>Off</code>. */ wrapping : {type : "sap.ui.core.Wrapping", group : "Behavior", defaultValue : Wrapping.None}, /** * Indicates when the <code>value</code> property gets updated with the user changes. Setting it to <code>true</code> updates the <code>value</code> property whenever the user has modified the text shown on the text area. * @since 1.30 */ valueLiveUpdate : {type : "boolean", group : "Behavior", defaultValue : false}, /** * Indicates the ability of the control to automatically grow and shrink dynamically with its content. * <b>Note:</b> This property should not be used when the <code>height</code> property is set. * @since 1.38.0 */ growing : {type : "boolean", group : "Behavior", defaultValue : false}, /** * Defines the maximum number of lines that the control can grow. * @since 1.38.0 */ growingMaxLines : {type : "int", group : "Behavior", defaultValue : 0} }, aggregations: { // The hidden aggregation for internal usage _counter: {type: "sap.m.Text", multiple: false, visibility: "hidden"} }, events : { /** * Is fired whenever the user has modified the text shown on the text area. */ liveChange : { parameters : { /** * The new <code>value</code> of the control. */ value : {type : "string"} } } }, dnd: { draggable: false, droppable: true } }, renderer: TextAreaRenderer }); /** * Initializes the control. */ TextArea.prototype.init = function(){ var oCounter; InputBase.prototype.init.call(this); this.sResizeListenerId = null; this._bPasteIsTriggered = false; oCounter = new Text(this.getId() + '-counter', {}).addStyleClass("sapMTextAreaCounter").setVisible(false); this.setAggregation("_counter", oCounter); }; TextArea.prototype.setShowExceededText = function (bValue) { var oCounter = this.getAggregation("_counter"), sValue; if (bValue) { if (this.getAriaLabelledBy().indexOf(oCounter.getId()) < 0) { this.addAriaLabelledBy(oCounter.getId()); } } else { //remove the counter from AriaLabelledBy oCounter = this.getAggregation("_counter"); oCounter && this.removeAriaLabelledBy(oCounter.getId()); // respect to max length sValue = this.getValue(); if (this.getMaxLength()) { sValue = sValue.substring(0, this.getMaxLength()); this.setValue(sValue); } } oCounter.setVisible(bValue); this.setProperty("showExceededText", bValue); this._updateMaxLengthAttribute(); return this; }; TextArea.prototype.exit = function() { InputBase.prototype.exit.call(this); jQuery(window).off("resize.sapMTextAreaGrowing"); this._detachResizeHandler(); this._deregisterEvents(); }; TextArea.prototype.onBeforeRendering = function() { InputBase.prototype.onBeforeRendering.call(this); var oCounter = this.getAggregation("_counter"); if ((this.getMaxLength() <= 0 || !this.getShowExceededText()) && oCounter.getVisible()) { oCounter.setVisible(false); } this._detachResizeHandler(); if (this.getGrowing()) { jQuery(window).on("resize.sapMTextAreaGrowing", this._updateOverflow.bind(this)); } else { jQuery(window).off("resize.sapMTextAreaGrowing"); } }; // Attach listeners on after rendering and find iscroll TextArea.prototype.onAfterRendering = function() { InputBase.prototype.onAfterRendering.call(this); if (this.getGrowing()) { // Register resize event this._sResizeListenerId = ResizeHandler.register(this, this._resizeHandler.bind(this)); // adjust textarea height if (this.getGrowingMaxLines() > 0) { this._setGrowingMaxHeight(); } this._adjustContainerDimensions(); } this._updateMaxLengthAttribute(); if (!Device.support.touch) { return; } // check behaviour mode var $TextArea = this.$("inner"); if (this._behaviour.INSIDE_SCROLLABLE_WITHOUT_FOCUS) { // Bind browser events to mimic native scrolling $TextArea.on("touchstart", jQuery.proxy(this._onTouchStart, this)); $TextArea.on("touchmove", jQuery.proxy(this._onTouchMove, this)); } else if (this._behaviour.PAGE_NON_SCROLLABLE_AFTER_FOCUS) { // stop bubbling to disable preventDefault calls $TextArea.on("touchmove", function(e) { if ($TextArea.is(":focus")) { e.stopPropagation(); } }); } }; /** * Removes the <code>touchstart</code> and <code>touchmove</code> custom events * binded to the textarea in <code>.onAfterRendering</code>. * * The semantic renderer does NOT remove DOM structure from the DOM tree; * therefore all custom events, including the ones that are registered * with jQuery, must be deregistered correctly at the <code>.onBeforeRendering</code> * and exit hooks. * * @private * */ TextArea.prototype._deregisterEvents = function() { this.$("inner").off("touchstart").off("touchmove"); }; /** * Sets the maximum height of the HTML textarea. * This is the actual implementation of growingMaxLines property. * * @private */ TextArea.prototype._setGrowingMaxHeight = function () { var oHiddenDiv = this.getDomRef('hidden'), oLoadedLibraries = Lib.all(), fLineHeight, fMaxHeight, oStyle; // The CSS rules might not hve been applied yet and the getComputedStyle function might not return the proper values. So, wait for the theme to be applied properly // The check for loaded libraries is to ensure that sap.m has been loaded. TextArea's CSS sources would be loaded along with the library if (!oLoadedLibraries || !oLoadedLibraries['sap.m']) { Theming.attachApplied(this._setGrowingMaxHeight.bind(this)); return; } // After it's been executed, we need to release the resources Theming.detachApplied(this._setGrowingMaxHeight); oStyle = window.getComputedStyle(oHiddenDiv); fLineHeight = this._getLineHeight(); fMaxHeight = (fLineHeight * this.getGrowingMaxLines()) + parseFloat(oStyle.getPropertyValue("padding-top")) + parseFloat(oStyle.getPropertyValue("border-top-width")) + parseFloat(oStyle.getPropertyValue("border-bottom-width")); // bottom padding is out of scrolling content in firefox if (Device.browser.firefox) { fMaxHeight += parseFloat(oStyle.getPropertyValue("padding-bottom")); } oHiddenDiv.style.maxHeight = fMaxHeight + "px"; }; /** * Calculates the line height of the HTML textarea in px. * * @returns {float|null} The line height in px * @private */ TextArea.prototype._getLineHeight = function () { var oTextAreaRef = this.getFocusDomRef(), oStyle; if (!oTextAreaRef) { return; } oStyle = window.getComputedStyle(oTextAreaRef); return isNaN(parseFloat(oStyle.getPropertyValue("line-height"))) ? 1.4 * parseFloat(oStyle.getPropertyValue("font-size")) : parseFloat(oStyle.getPropertyValue("line-height")); }; /** * Function is called when TextArea is resized * * @param {jQuery.Event} oEvent The event object * @private */ TextArea.prototype._resizeHandler = function (oEvent) { this._adjustContainerDimensions(); }; /** * Detaches the resize handler * @private */ TextArea.prototype._detachResizeHandler = function () { if (this._sResizeListenerId) { ResizeHandler.deregister(this._sResizeListenerId); this._sResizeListenerId = null; } }; // overwrite the input base enter handling for change event TextArea.prototype.onsapenter = function(oEvent) { oEvent.setMarked(); }; // Overwrite input base revert handling for escape // to fire own liveChange event and property set TextArea.prototype.onValueRevertedByEscape = function(sValue) { // update value property if needed if (this.getValueLiveUpdate()) { this.setProperty("value", sValue, true); // get the value back maybe there is a formatter sValue = this.getValue(); } this.fireLiveChange({ value: sValue, // backwards compatibility newValue: sValue }); }; TextArea.prototype.getValue = function() { var oTextAreaRef = this.getFocusDomRef(); return oTextAreaRef ? oTextAreaRef.value : this.getProperty("value"); }; TextArea.prototype.setValue = function (sValue) { InputBase.prototype.setValue.call(this, sValue); this._handleShowExceededText(); if (this.getGrowing()) { this._adjustContainerDimensions(); } return this; }; // mark the event that it is handled by the textarea TextArea.prototype.onsapnext = function(oEvent) { oEvent.setMarked(); }; // mark the event that it is handled by the textarea TextArea.prototype.onsapprevious = function(oEvent) { oEvent.setMarked(); }; TextArea.prototype.oninput = function(oEvent) { InputBase.prototype.oninput.call(this, oEvent); /* TODO remove after the end of support for Internet Explorer */ // Handles paste. This is before checking for "invalid" because in IE after paste the event is set as "invalid" if (this._bPasteIsTriggered) { this._bPasteIsTriggered = false; this._selectExceededText(); } if (oEvent.isMarked("invalid")) { return; } var oTextAreaRef = this.getFocusDomRef(), sValue = oTextAreaRef.value, bShowExceededText = this.getShowExceededText(), iMaxLength = this.getMaxLength(); if (!bShowExceededText && !this._bIsComposingCharacter && iMaxLength && sValue.length > iMaxLength) { sValue = sValue.substring(0, iMaxLength); oTextAreaRef.value = sValue; } // update value property if needed if (this.getValueLiveUpdate()) { this.setProperty("value", sValue, true); // get the value back maybe there is a formatter sValue = this.getValue(); } this._handleShowExceededText(); // handle growing if (this.getGrowing()) { this._adjustContainerDimensions(); } this.fireLiveChange({ value: sValue, // backwards compatibility newValue: sValue }); }; /** * Handles the onpaste event. * * The event is customized and the <code>textArea</code> value is calculated manually * because when the <code>showExceededText</code> is set to * <code>true</code> the exceeded text should be selected on paste. * @param {jQuery.Event} oEvent The event object */ TextArea.prototype.onpaste = function (oEvent) { if (this.getShowExceededText()) { this._bPasteIsTriggered = true; } }; TextArea.prototype._adjustContainerDimensions = function() { var oTextAreaRef = this.getFocusDomRef(), oHiddenDiv = this.getDomRef("hidden"), sHiddenDivMinHeight, sNeededMinHeight; if (!oTextAreaRef || !oHiddenDiv) { return; } oHiddenDiv.style.width = ""; if (this.getGrowing() && !this.getWidth() && /* width property overwrites cols */ this.getCols() !== 20 /* the default value */) { oHiddenDiv.style.width = (this.getCols() * 0.5) + "rem"; } sHiddenDivMinHeight = oHiddenDiv.style["min-height"]; sNeededMinHeight = this.getRows() * this._getLineHeight() + "px"; // change the min-height of the mirror div, // depending on the rows, only if needed if (!sHiddenDivMinHeight || sNeededMinHeight !== sHiddenDivMinHeight) { oHiddenDiv.style["min-height"] = sNeededMinHeight; } // ensure that there will be left space if the last row is a new line // ensure that possible html/script content is escaped oHiddenDiv.innerHTML = encodeXML(oTextAreaRef.value) + '&nbsp;'; this._updateOverflow(); }; TextArea.prototype._updateOverflow = function() { var oTextAreaRef = this.getFocusDomRef(), oHiddenDiv = this.getDomRef("hidden"), fMaxHeight; if (oTextAreaRef) { fMaxHeight = parseFloat(window.getComputedStyle(oHiddenDiv)["max-height"]); oTextAreaRef.style.overflowY = (oHiddenDiv.scrollHeight > fMaxHeight) ? "auto" : ""; } }; TextArea.prototype._getInputValue = function(sValue) { //not calling InputBase.prototype._getInputValue because it always respects maxValue sValue = (sValue === undefined) ? this.$("inner").val() || "" : sValue.toString(); if (this.getMaxLength() > 0 && !this.getShowExceededText()) { sValue = sValue.substring(0, this.getMaxLength()); } return sValue.replace(/\r\n/g, "\n"); }; TextArea.prototype._selectExceededText = function () { var iValueLength = this.getValue().length; if (iValueLength > this.getMaxLength()) { this.selectText(this.getMaxLength(), iValueLength); } }; TextArea.prototype._updateMaxLengthAttribute = function () { var oTextAreaRef = this.getFocusDomRef(); if (!oTextAreaRef) { return; } if (this.getShowExceededText()) { oTextAreaRef.removeAttribute("maxlength"); this._handleShowExceededText(); } else { this.getMaxLength() && oTextAreaRef.setAttribute("maxlength", this.getMaxLength()); } }; /** * Updates counter value. * @private */ TextArea.prototype._handleShowExceededText = function () { var oCounter = this.getAggregation("_counter"), iMaxLength = this.getMaxLength(), sCounterText; if (!this.getDomRef() || !this.getShowExceededText() || !iMaxLength) { return; } sCounterText = this._getCounterValue(); oCounter.setText(sCounterText); }; /** * Checks if the TextArea has exceeded the value for MaxLength * @param {number} bValue The value * @returns {boolean} True if the text area exceeded the max length * @private */ TextArea.prototype._maxLengthIsExceeded = function (bValue) { var bResult = false; if (this.getMaxLength() > 0 && this.getShowExceededText() && this.getValue().length > this.getMaxLength()) { bResult = true; } return bResult; }; TextArea.prototype._getCounterValue = function () { var oBundle = Lib.getResourceBundleFor("sap.m"), iCharactersExceeded = this.getMaxLength() - this.getValue().length, bExceeded = (iCharactersExceeded < 0 ? true : false), sMessageBundleKey = "TEXTAREA_CHARACTER" + ( Math.abs(iCharactersExceeded) === 1 ? "" : "S") + "_" + (bExceeded ? "EXCEEDED" : "LEFT"); return oBundle.getText(sMessageBundleKey, [Math.abs(iCharactersExceeded)]); }; /** * Some browsers let us to scroll inside of the textarea without focusing. * Android is very buggy and no touch event is publishing after focus. * Android 4.1+ has touch events but page scroll is not possible after * we reached the edge(bottom, top) of the textarea * * @private */ TextArea.prototype._behaviour = (function(oDevice) { return { INSIDE_SCROLLABLE_WITHOUT_FOCUS : oDevice.os.ios || oDevice.browser.chrome, PAGE_NON_SCROLLABLE_AFTER_FOCUS : oDevice.os.android }; }(Device)); /** * On touch start get iscroll and save starting point * * @private * @param {jQuery.Event} oEvent The event object */ TextArea.prototype._onTouchStart = function(oEvent) { var oTouchEvent = oEvent.touches[0]; this._iStartY = oTouchEvent.pageY; this._iStartX = oTouchEvent.pageX; this._bHorizontalScroll = undefined; // disable swipe handling of jQuery-mobile since it calls preventDefault // on touchmove and this can break the scrolling nature of the textarea oEvent.setMarked("swipestartHandled"); }; /** * Touch move listener doing native scroll workaround * * @private * @param {jQuery.Event} oEvent The event object */ TextArea.prototype._onTouchMove = function(oEvent) { var oTextAreaRef = this.getFocusDomRef(), iPageY = oEvent.touches[0].pageY, iScrollTop = oTextAreaRef.scrollTop, bTop = iScrollTop <= 0, bBottom = iScrollTop + oTextAreaRef.clientHeight >= oTextAreaRef.scrollHeight, bGoingUp = this._iStartY > iPageY, bGoingDown = this._iStartY < iPageY, bOnEnd = bTop && bGoingDown || bBottom && bGoingUp; if (this._bHorizontalScroll === undefined) { // check once this._bHorizontalScroll = Math.abs(this._iStartY - iPageY) < Math.abs(this._iStartX - oEvent.touches[0].pageX); } if (this._bHorizontalScroll || !bOnEnd) { // to prevent the rubber-band effect we are calling prevent default on touchmove // from jquery.sap.mobile but this breaks the scrolling nature of the textarea oEvent.setMarked(); } }; /** * Special handling for Enter key which triggers the FieldGroupNavigation on Enter. This treatment is only relevant * for the Enter key itself, as this is used in TextArea to start a new line. * @param {jQuery.Event} oEvent The event object * @private */ TextArea.prototype.onkeyup = function(oEvent) { if (oEvent.keyCode === KeyCodes.ENTER) { oEvent.setMarked("enterKeyConsumedAsContent"); } }; return TextArea; });