@qooxdoo/framework
Version:
The JS Framework for Coders
1,082 lines (909 loc) • 29.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)
* 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 : {
/** Stylesheet needed to style the native placeholder element. */
__stylesheet : null,
/**
* Adds the CSS rules needed to style the native placeholder element.
*/
__addPlaceholderRules : function() {
var engine = qx.core.Environment.get("engine.name");
var browser = qx.core.Environment.get("browser.name");
var colorManager = qx.theme.manager.Color.getInstance();
var color = colorManager.resolve("text-placeholder");
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";
}
qx.ui.style.Stylesheet.getInstance().addRule(selector, "color: " + color + " !important");
} else if (engine == "webkit" && browser != "edge") {
selector = "input.qx-placeholder-color::-webkit-input-placeholder, textarea.qx-placeholder-color::-webkit-input-placeholder";
qx.ui.style.Stylesheet.getInstance().addRule(selector, "color: " + color);
} 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);
qx.ui.style.Stylesheet.getInstance().addRule(selector, "color: " + color + " !important");
}
}
},
/*
*****************************************************************************
CONSTRUCTOR
*****************************************************************************
*/
/**
* @param value {String} initial text value of the input field ({@link #setValue}).
*/
construct : function(value)
{
this.base(arguments);
// shortcut for placeholder feature detection
this.__useQxPlaceholder = !qx.core.Environment.get("css.placeholder");
if (value != null) {
this.setValue(value);
}
this.getContentElement().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")) {
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
},
/**
* 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
*****************************************************************************
*/
members :
{
__nullValue : true,
_placeholder : null,
__oldValue : null,
__oldInputValue : null,
__useQxPlaceholder : true,
__font : null,
__webfontListenerId : null,
/*
---------------------------------------------------------------------------
WIDGET API
---------------------------------------------------------------------------
*/
// overridden
getFocusElement : function() {
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 : function() {
return new qx.html.Input("text");
},
// overridden
renderLayout : function(left, top, width, height)
{
var updateInsets = this._updateInsets;
var changes = this.base(arguments, 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 : function(innerHeight, element) {
//use it in child classes
},
// overridden
_createContentElement : function()
{
// 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 : function(value, old)
{
this.base(arguments, 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 : function()
{
return {
width : this.__textSize.width * 10,
height : this.__textSize.height || 16
};
},
// overridden
_applyFont : function(value, old)
{
if (old && this.__font && this.__webfontListenerId) {
this.__font.removeListenerById(this.__webfontListenerId);
this.__webfontListenerId = null;
}
// Apply
var styles;
if (value)
{
this.__font = qx.theme.manager.Font.getInstance().resolve(value);
if (this.__font instanceof qx.bom.webfonts.WebFont) {
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 : function(value, old)
{
if (value) {
this.getContentElement().setStyle(
"color", qx.theme.manager.Color.getInstance().resolve(value)
);
} else {
this.getContentElement().removeStyle("color");
}
},
// property apply
_applyMaxLength : function(value, old) {
if (value) {
this.getContentElement().setAttribute("maxLength", value);
} else {
this.getContentElement().removeAttribute("maxLength");
}
},
// overridden
tabFocus : function() {
this.base(arguments);
this.selectAllText();
},
/**
* Returns the text size.
* @return {Map} The text size.
*/
_getTextSize : function() {
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 : function(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);
}
}
},
/**
* Triggers text size recalculation after a web font was loaded
*
* @param ev {qx.event.type.Data} "changeStatus" event
*/
_onWebFontStatusChange : function(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 : function(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 : function(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 : function() {
return (this.isDisposed() || this.__nullValue) ? null : this.getContentElement().getValue();
},
/**
* Resets the value to the default
*/
resetValue : function() {
this.setValue(null);
},
/**
* Event listener for change event of content element
*
* @param e {qx.event.type.Data} Incoming change event
*/
_onChangeContent : function(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 : function() {
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 : function() {
return this.getContentElement().getTextSelectionLength();
},
/**
* Returns the start of the text selection
*
* @return {Integer|null} Start of selection or null if not available
*/
getTextSelectionStart : function() {
return this.getContentElement().getTextSelectionStart();
},
/**
* Returns the end of the text selection
*
* @return {Integer|null} End of selection or null if not available
*/
getTextSelectionEnd : function() {
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 : function(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 : function() {
this.getContentElement().clearTextSelection();
},
/**
* Selects the whole content
*
*/
selectAllText : function() {
this.setTextSelection(0);
},
/*
---------------------------------------------------------------------------
PLACEHOLDER HELPERS
---------------------------------------------------------------------------
*/
// overridden
setLayoutParent : function(parent)
{
this.base(arguments, 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 : function()
{
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 : function() {
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: function() {
if (this.hasState("showingPlaceholder")) {
if (this.__useQxPlaceholder) {
this._getPlaceholderElement().setStyle("visibility", "hidden");
}
this.removeState("showingPlaceholder");
}
},
/**
* Updates the placeholder text with the DOM
*/
_syncPlaceholder : function ()
{
if (this.hasState("showingPlaceholder") && this.__useQxPlaceholder) {
this._getPlaceholderElement().setStyle("visibility", "visible");
}
},
/**
* Returns the placeholder label and creates it if necessary.
*/
_getPlaceholderElement : function()
{
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" : function(e)
{
var content = this.getPlaceholder();
if (content && content.translate) {
this.setPlaceholder(content.translate());
}
},
"false" : null
}),
// overridden
_onChangeTheme : function() {
this.base(arguments);
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.__stylesheet) {
qx.bom.Stylesheet.removeSheet(qx.ui.form.AbstractField.__stylesheet);
qx.ui.form.AbstractField.__stylesheet = null;
qx.ui.form.AbstractField.__addPlaceholderRules();
}
},
/**
* Validates the the input value.
*
* @param value {Object} The value to check
* @returns The checked value
*/
_validateInput : function(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 : function(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", function() {
this.getContentElement().getDomElement().removeAttribute("placeholder");
this.getContentElement().getDomElement().setAttribute("placeholder", value);
}, this);
}
}
}
},
// property apply
_applyTextAlign : function(value, old) {
this.getContentElement().setStyle("textAlign", value);
},
// property apply
_applyReadOnly : function(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 : function(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 : function()
{
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")) {
qx.locale.Manager.getInstance().removeListener("changeLocale", this._onChangeLocale, this);
}
if (this.__font && this.__webfontListenerId) {
this.__font.removeListenerById(this.__webfontListenerId);
}
this.getContentElement().removeListener("input", this._onHtmlInput, this);
}
});