UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

1,090 lines (961 loc) 31.3 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) * Fabian Jakobs (fjakobs) ************************************************************************ */ /** * This is a basic form field with common functionality for * {@link TextArea} and {@link TextField}. * * On every keystroke the value is synchronized with the * value of the textfield. Value changes can be monitored by listening to the * {@link #input} or {@link #changeValue} events, respectively. */ qx.Class.define("qx.ui.form.AbstractField", { extend: qx.ui.core.Widget, implement: [qx.ui.form.IStringForm, qx.ui.form.IForm], include: [qx.ui.form.MForm], type: "abstract", statics: { __addedPlaceholderRules: "", /** * Adds the CSS rules needed to style the native placeholder element. */ __addPlaceholderRules() { const theme = qx.theme.manager.Meta.getInstance().getTheme(); const currentThemeName = theme ? theme.title || theme.name : ""; if (qx.ui.form.AbstractField.__addedPlaceholderRules === currentThemeName) { return; } qx.ui.form.AbstractField.__addedPlaceholderRules = currentThemeName; var engine = qx.core.Environment.get("engine.name"); var browser = qx.core.Environment.get("browser.name"); var colorManager = qx.theme.manager.Color.getInstance(); var appearanceProperties = qx.theme.manager.Appearance.getInstance().styleFrom("textfield", { showingPlaceholder: true }); var styles = {}; var color = null; var font = null; if (appearanceProperties) { color = appearanceProperties["textColor"] ? colorManager.resolve(appearanceProperties["textColor"]) : null; font = appearanceProperties["font"] ? qx.theme.manager.Font.getInstance().resolve( appearanceProperties["font"] ) : null; } if (!color) { color = colorManager.resolve("text-placeholder"); } if (color) { styles.color = color + " !important"; } if (font) { qx.lang.Object.mergeWith(styles, font.getStyles(), false); } var selector; if (engine == "gecko") { // see https://developer.mozilla.org/de/docs/CSS/:-moz-placeholder for details if (parseFloat(qx.core.Environment.get("engine.version")) >= 19) { selector = "input::-moz-placeholder, textarea::-moz-placeholder"; } else { selector = "input:-moz-placeholder, textarea:-moz-placeholder"; } } else if (engine == "webkit" && browser != "edge") { selector = "input.qx-placeholder-color::-webkit-input-placeholder, textarea.qx-placeholder-color::-webkit-input-placeholder"; } else if (engine == "mshtml" || browser == "edge") { var separator = browser == "edge" ? "::" : ":"; selector = [ "input.qx-placeholder-color", "-ms-input-placeholder, textarea.qx-placeholder-color", "-ms-input-placeholder" ].join(separator); } if(qx.ui.style.Stylesheet.getInstance().hasRule(selector)) { qx.ui.style.Stylesheet.getInstance().removeRule(selector); } qx.ui.style.Stylesheet.getInstance().addRule( selector, "color: " + color + " !important" ); } }, /* ***************************************************************************** CONSTRUCTOR ***************************************************************************** */ /** * @param value {String} initial text value of the input field ({@link #setValue}). */ construct(value) { super(); // shortcut for placeholder feature detection this.__useQxPlaceholder = !qx.core.Environment.get("css.placeholder"); if (value != null) { this.setValue(value); } let el = this.getContentElement(); el.addListener("change", this._onChangeContent, this); // use qooxdoo placeholder if no native placeholder is supported if (this.__useQxPlaceholder) { // assign the placeholder text after the appearance has been applied this.addListener("syncAppearance", this._syncPlaceholder, this); } else { // add rules for native placeholder color qx.ui.form.AbstractField.__addPlaceholderRules(); // add a class to the input to restrict the placeholder color this.getContentElement().addClass("qx-placeholder-color"); } // translation support if (qx.core.Environment.get("qx.dynlocale")) { this.__changeLocaleAbstractFieldListenerId = qx.locale.Manager.getInstance().addListener( "changeLocale", this._onChangeLocale, this ); } }, /* ***************************************************************************** EVENTS ***************************************************************************** */ events: { /** * The event is fired on every keystroke modifying the value of the field. * * The method {@link qx.event.type.Data#getData} returns the * current value of the text field. */ input: "qx.event.type.Data", /** * The event is fired each time the text field looses focus and the * text field values has changed. * * If you change {@link #liveUpdate} to true, the changeValue event will * be fired after every keystroke and not only after every focus loss. In * that mode, the changeValue event is equal to the {@link #input} event. * * The method {@link qx.event.type.Data#getData} returns the * current text value of the field. */ changeValue: "qx.event.type.Data" }, /* ***************************************************************************** PROPERTIES ***************************************************************************** */ properties: { /** * Alignment of the text */ textAlign: { check: ["left", "center", "right"], nullable: true, themeable: true, apply: "_applyTextAlign" }, /** Whether the field is read only */ readOnly: { check: "Boolean", apply: "_applyReadOnly", event: "changeReadOnly", init: false }, // overridden selectable: { refine: true, init: true }, // overridden focusable: { refine: true, init: true }, /** Maximal number of characters that can be entered in the TextArea. */ maxLength: { apply: "_applyMaxLength", check: "PositiveInteger", init: Infinity }, /** * Whether the {@link #changeValue} event should be fired on every key * input. If set to true, the changeValue event is equal to the * {@link #input} event. */ liveUpdate: { check: "Boolean", init: false }, /** * Fire a {@link #changeValue} event whenever the content of the * field matches the given regular expression. Accepts both regular * expression objects as well as strings for input. */ liveUpdateOnRxMatch: { check: "RegExp", transform: "_string2RegExp", init: null }, /** * String value which will be shown as a hint if the field is all of: * unset, unfocused and enabled. Set to null to not show a placeholder * text. */ placeholder: { check: "String", nullable: true, apply: "_applyPlaceholder" }, /** * RegExp responsible for filtering the value of the textfield. the RegExp * gives the range of valid values. * Note: The regexp specified is applied to each character in turn, * NOT to the entire string. So only regular expressions matching a * single character make sense in the context. * The following example only allows digits in the textfield. * <pre class='javascript'>field.setFilter(/[0-9]/);</pre> */ filter: { check: "RegExp", nullable: true, init: null } }, /* ***************************************************************************** MEMBERS ***************************************************************************** */ /* eslint-disable @qooxdoo/qx/no-refs-in-members */ members: { __nullValue: true, _placeholder: null, __oldValue: null, __oldInputValue: null, __useQxPlaceholder: true, __font: null, __webfontListenerId: null, /* --------------------------------------------------------------------------- WIDGET API --------------------------------------------------------------------------- */ // overridden getFocusElement() { var el = this.getContentElement(); if (el) { return el; } }, /** * Creates the input element. Derived classes may override this * method, to create different input elements. * * @return {qx.html.Input} a new input element. */ _createInputElement() { return new qx.html.Input("text"); }, // overridden renderLayout(left, top, width, height) { var updateInsets = this._updateInsets; var changes = super.renderLayout(left, top, width, height); // Directly return if superclass has detected that no // changes needs to be applied if (!changes) { return; } var inner = changes.size || updateInsets; var pixel = "px"; if (inner || changes.local || changes.margin) { var innerWidth = width; var innerHeight = height; } var input = this.getContentElement(); // we don't need to update positions on native placeholders if (updateInsets && this.__useQxPlaceholder) { if (this.__useQxPlaceholder) { var insets = this.getInsets(); this._getPlaceholderElement().setStyles({ paddingTop: insets.top + pixel, paddingRight: insets.right + pixel, paddingBottom: insets.bottom + pixel, paddingLeft: insets.left + pixel }); } } if (inner || changes.margin) { // we don't need to update dimensions on native placeholders if (this.__useQxPlaceholder) { var insets = this.getInsets(); this._getPlaceholderElement().setStyles({ width: innerWidth - insets.left - insets.right + pixel, height: innerHeight - insets.top - insets.bottom + pixel }); } input.setStyles({ width: innerWidth + pixel, height: innerHeight + pixel }); this._renderContentElement(innerHeight, input); } if (changes.position) { if (this.__useQxPlaceholder) { this._getPlaceholderElement().setStyles({ left: left + pixel, top: top + pixel }); } } }, /** * Hook into {@link qx.ui.form.AbstractField#renderLayout} method. * Called after the contentElement has a width and an innerWidth. * * Note: This was introduced to fix BUG#1585 * * @param innerHeight {Integer} The inner height of the element. * @param element {Element} The element. */ _renderContentElement(innerHeight, element) { //use it in child classes }, // overridden _createContentElement() { // create and add the input element var el = this._createInputElement(); // initialize the html input el.setSelectable(this.getSelectable()); el.setEnabled(this.getEnabled()); // Add listener for input event el.addListener("input", this._onHtmlInput, this); // Disable HTML5 spell checking el.setAttribute("spellcheck", "false"); el.addClass("qx-abstract-field"); // IE8 in standard mode needs some extra love here to receive events. if ( qx.core.Environment.get("engine.name") == "mshtml" && qx.core.Environment.get("browser.documentmode") == 8 ) { el.setStyles({ backgroundImage: "url(" + qx.util.ResourceManager.getInstance().toUri("qx/static/blank.gif") + ")" }); } return el; }, // overridden _applyEnabled(value, old) { super._applyEnabled(value, old); this.getContentElement().setEnabled(value); if (this.__useQxPlaceholder) { if (value) { this._showPlaceholder(); } else { this._removePlaceholder(); } } else { var input = this.getContentElement(); // remove the placeholder on disabled input elements input.setAttribute("placeholder", value ? this.getPlaceholder() : ""); } }, // default text sizes /** * @lint ignoreReferenceField(__textSize) */ __textSize: { width: 16, height: 16 }, // overridden _getContentHint() { return { width: this.__textSize.width * 10, height: this.__textSize.height || 16 }; }, // overridden _applyFont(value, old) { if (old && this.__font && this.__webfontListenerId) { this.__font.removeListenerById(this.__webfontListenerId); this.__webfontListenerId = null; } // Apply var styles; if (value) { if (qx.lang.Type.isString(value)) { value = qx.theme.manager.Font.getInstance().resolve(value); } this.__font = value; if ( this.__font instanceof qx.bom.webfonts.WebFont && !this.__font.isValid() ) { this.__webfontListenerId = this.__font.addListener( "changeStatus", this._onWebFontStatusChange, this ); } styles = this.__font.getStyles(); } else { styles = qx.bom.Font.getDefaultStyles(); } // check if text color already set - if so this local value has higher priority if (this.getTextColor() != null) { delete styles["color"]; } // apply the font to the content element // IE 8 - 10 (but not 11 Preview) will ignore the lineHeight value // unless it's applied directly. if ( qx.core.Environment.get("engine.name") == "mshtml" && qx.core.Environment.get("browser.documentmode") < 11 ) { qx.html.Element.flush(); this.getContentElement().setStyles(styles, true); } else { this.getContentElement().setStyles(styles); } // the font will adjust automatically on native placeholders if (this.__useQxPlaceholder) { // don't apply the color to the placeholder delete styles["color"]; // apply the font to the placeholder this._getPlaceholderElement().setStyles(styles); } // Compute text size if (value) { this.__textSize = qx.bom.Label.getTextSize("A", styles); } else { delete this.__textSize; } // Update layout qx.ui.core.queue.Layout.add(this); }, // overridden _applyTextColor(value, old) { if (value) { this.getContentElement().setStyle( "color", qx.theme.manager.Color.getInstance().resolve(value) ); } else { this.getContentElement().removeStyle("color"); } }, // property apply _applyMaxLength(value, old) { if (value) { this.getContentElement().setAttribute("maxLength", value); } else { this.getContentElement().removeAttribute("maxLength"); } }, // property transform _string2RegExp(value, old) { if (qx.lang.Type.isString(value)) { value = new RegExp(value); } return value; }, // overridden tabFocus() { super.tabFocus(); this.selectAllText(); }, /** * Returns the text size. * @return {Map} The text size. */ _getTextSize() { return this.__textSize; }, /* --------------------------------------------------------------------------- EVENTS --------------------------------------------------------------------------- */ /** * Event listener for native input events. Redirects the event * to the widget. Also checks for the filter and max length. * * @param e {qx.event.type.Data} Input event */ _onHtmlInput(e) { var value = e.getData(); var fireEvents = true; this.__nullValue = false; // value unchanged; Firefox fires "input" when pressing ESC [BUG #5309] if (this.__oldInputValue && this.__oldInputValue === value) { fireEvents = false; } // check for the filter if (this.getFilter() != null) { var filteredValue = this._validateInput(value); if (filteredValue != value) { fireEvents = this.__oldInputValue !== filteredValue; value = filteredValue; this.getContentElement().setValue(value); } } // fire the events, if necessary if (fireEvents) { // store the old input value this.fireDataEvent("input", value, this.__oldInputValue); this.__oldInputValue = value; // check for the live change event if (this.getLiveUpdate()) { this.__fireChangeValueEvent(value); } // check for the liveUpdateOnRxMatch change event else { var fireRx = this.getLiveUpdateOnRxMatch(); if (fireRx && value.match(fireRx)) { this.__fireChangeValueEvent(value); } } } }, /** * Triggers text size recalculation after a web font was loaded * * @param ev {qx.event.type.Data} "changeStatus" event */ _onWebFontStatusChange(ev) { if (ev.getData().valid === true) { var styles = this.__font.getStyles(); this.__textSize = qx.bom.Label.getTextSize("A", styles); qx.ui.core.queue.Layout.add(this); } }, /** * Handles the firing of the changeValue event including the local cache * for sending the old value in the event. * * @param value {String} The new value. */ __fireChangeValueEvent(value) { var old = this.__oldValue; this.__oldValue = value; if (old != value) { this.fireNonBubblingEvent("changeValue", qx.event.type.Data, [ value, old ]); } }, /* --------------------------------------------------------------------------- TEXTFIELD VALUE API --------------------------------------------------------------------------- */ /** * Sets the value of the textfield to the given value. * * @param value {String} The new value */ setValue(value) { if (this.isDisposed()) { return null; } // handle null values if (value === null) { // just do nothing if null is already set if (this.__nullValue) { return value; } value = ""; this.__nullValue = true; } else { this.__nullValue = false; // native placeholders will be removed by the browser if (this.__useQxPlaceholder) { this._removePlaceholder(); } } if (qx.lang.Type.isString(value)) { var elem = this.getContentElement(); if (elem.getValue() != value) { var oldValue = elem.getValue(); elem.setValue(value); var data = this.__nullValue ? null : value; this.__oldValue = oldValue; this.__fireChangeValueEvent(data); // reset the input value on setValue calls [BUG #6892] this.__oldInputValue = this.__oldValue; } // native placeholders will be shown by the browser if (this.__useQxPlaceholder) { this._showPlaceholder(); } return value; } throw new Error("Invalid value type: " + value); }, /** * Returns the current value of the textfield. * * @return {String|null} The current value */ getValue() { return this.isDisposed() || this.__nullValue ? null : this.getContentElement().getValue(); }, /** * Resets the value to the default */ resetValue() { this.setValue(null); }, /** * Event listener for change event of content element * * @param e {qx.event.type.Data} Incoming change event */ _onChangeContent(e) { this.__nullValue = e.getData() === null; this.__fireChangeValueEvent(e.getData()); }, /* --------------------------------------------------------------------------- TEXTFIELD SELECTION API --------------------------------------------------------------------------- */ /** * Returns the current selection. * This method only works if the widget is already created and * added to the document. * * @return {String|null} */ getTextSelection() { return this.getContentElement().getTextSelection(); }, /** * Returns the current selection length. * This method only works if the widget is already created and * added to the document. * * @return {Integer|null} */ getTextSelectionLength() { return this.getContentElement().getTextSelectionLength(); }, /** * Returns the start of the text selection * * @return {Integer|null} Start of selection or null if not available */ getTextSelectionStart() { return this.getContentElement().getTextSelectionStart(); }, /** * Returns the end of the text selection * * @return {Integer|null} End of selection or null if not available */ getTextSelectionEnd() { return this.getContentElement().getTextSelectionEnd(); }, /** * Set the selection to the given start and end (zero-based). * If no end value is given the selection will extend to the * end of the textfield's content. * This method only works if the widget is already created and * added to the document. * * @param start {Integer} start of the selection (zero-based) * @param end {Integer} end of the selection */ setTextSelection(start, end) { this.getContentElement().setTextSelection(start, end); }, /** * Clears the current selection. * This method only works if the widget is already created and * added to the document. * */ clearTextSelection() { this.getContentElement().clearTextSelection(); }, /** * Selects the whole content * */ selectAllText() { this.setTextSelection(0); }, /* --------------------------------------------------------------------------- PLACEHOLDER HELPERS --------------------------------------------------------------------------- */ // overridden setLayoutParent(parent) { super.setLayoutParent(parent); if (this.__useQxPlaceholder) { if (parent) { this.getLayoutParent() .getContentElement() .add(this._getPlaceholderElement()); } else { var placeholder = this._getPlaceholderElement(); placeholder.getParent().remove(placeholder); } } }, /** * Helper to show the placeholder text in the field. It checks for all * states and possible conditions and shows the placeholder only if allowed. */ _showPlaceholder() { var fieldValue = this.getValue() || ""; var placeholder = this.getPlaceholder(); if ( placeholder != null && fieldValue == "" && !this.hasState("focused") && !this.hasState("disabled") ) { if (this.hasState("showingPlaceholder")) { this._syncPlaceholder(); } else { // the placeholder will be set as soon as the appearance is applied this.addState("showingPlaceholder"); } } }, /** * Remove the fake placeholder */ _onPointerDownPlaceholder() { window.setTimeout( function () { this.focus(); }.bind(this), 0 ); }, /** * Helper to remove the placeholder. Deletes the placeholder text from the * field and removes the state. */ _removePlaceholder() { if (this.hasState("showingPlaceholder")) { if (this.__useQxPlaceholder) { this._getPlaceholderElement().setStyle("visibility", "hidden"); } this.removeState("showingPlaceholder"); } }, /** * Updates the placeholder text with the DOM */ _syncPlaceholder() { if (this.hasState("showingPlaceholder") && this.__useQxPlaceholder) { this._getPlaceholderElement().setStyle("visibility", "visible"); } }, /** * Returns the placeholder label and creates it if necessary. */ _getPlaceholderElement() { if (this._placeholder == null) { // create the placeholder this._placeholder = new qx.html.Label(); var colorManager = qx.theme.manager.Color.getInstance(); this._placeholder.setStyles({ zIndex: 11, position: "absolute", color: colorManager.resolve("text-placeholder"), whiteSpace: "normal", // enable wrap by default cursor: "text", visibility: "hidden" }); this._placeholder.addListener( "pointerdown", this._onPointerDownPlaceholder, this ); } return this._placeholder; }, /** * Locale change event handler * * @signature function(e) * @param e {Event} the change event */ _onChangeLocale: qx.core.Environment.select("qx.dynlocale", { true(e) { var content = this.getPlaceholder(); if (content && content.translate) { this.setPlaceholder(content.translate()); } }, false: null }), // overridden _onChangeTheme() { super._onChangeTheme(); if (this._placeholder) { // delete the placeholder element because it uses a theme dependent color this._placeholder.dispose(); this._placeholder = null; } if (!this.__useQxPlaceholder) { qx.ui.form.AbstractField.__addPlaceholderRules(); } }, /** * Validates the the input value. * * @param value {Object} The value to check * @returns The checked value */ _validateInput(value) { var filteredValue = value; var filter = this.getFilter(); // If no filter is set return just the value if (filter !== null) { filteredValue = ""; var index = value.search(filter); var processedValue = value; while (index >= 0 && processedValue.length > 0) { filteredValue = filteredValue + processedValue.charAt(index); processedValue = processedValue.substring( index + 1, processedValue.length ); index = processedValue.search(filter); } } return filteredValue; }, /* --------------------------------------------------------------------------- PROPERTY APPLY ROUTINES --------------------------------------------------------------------------- */ // property apply _applyPlaceholder(value, old) { if (this.__useQxPlaceholder) { this._getPlaceholderElement().setValue(value); if (value != null) { this.addListener("focusin", this._removePlaceholder, this); this.addListener("focusout", this._showPlaceholder, this); this._showPlaceholder(); } else { this.removeListener("focusin", this._removePlaceholder, this); this.removeListener("focusout", this._showPlaceholder, this); this._removePlaceholder(); } } else { // only apply if the widget is enabled if (this.getEnabled()) { this.getContentElement().setAttribute("placeholder", value); if ( qx.core.Environment.get("browser.name") === "firefox" && parseFloat(qx.core.Environment.get("browser.version")) < 36 && this.getContentElement().getNodeName() === "textarea" && !this.getContentElement().getDomElement() ) { /* qx Bug #8870: Firefox 35 will not display a text area's placeholder text if the attribute is set before the element is added to the DOM. This is fixed in FF 36. */ this.addListenerOnce("appear", () => { this.getContentElement() .getDomElement() .removeAttribute("placeholder"); this.getContentElement() .getDomElement() .setAttribute("placeholder", value); }); } } } }, // property apply _applyTextAlign(value, old) { this.getContentElement().setStyle("textAlign", value); }, // property apply _applyReadOnly(value, old) { var element = this.getContentElement(); element.setAttribute("readOnly", value); if (value) { this.addState("readonly"); this.setFocusable(false); } else { this.removeState("readonly"); this.setFocusable(true); } } }, defer(statics) { var css = "border: none;" + "padding: 0;" + "margin: 0;" + "display : block;" + "background : transparent;" + "outline: none;" + "appearance: none;" + "position: absolute;" + "autoComplete: off;" + "resize: none;" + "border-radius: 0;"; qx.ui.style.Stylesheet.getInstance().addRule(".qx-abstract-field", css); }, /* ***************************************************************************** DESTRUCTOR ***************************************************************************** */ destruct() { if (this._placeholder) { this._placeholder.removeListener( "pointerdown", this._onPointerDownPlaceholder, this ); var parent = this._placeholder.getParent(); if (parent) { parent.remove(this._placeholder); } this._placeholder.dispose(); } this._placeholder = this.__font = null; if (qx.core.Environment.get("qx.dynlocale") && this.__changeLocaleAbstractFieldListenerId) { qx.locale.Manager.getInstance().removeListenerById( this.__changeLocaleAbstractFieldListenerId ); } if (this.__font && this.__webfontListenerId) { this.__font.removeListenerById(this.__webfontListenerId); } this.getContentElement().removeListener("input", this._onHtmlInput, this); } });