/**
* @license
* Copyright The Closure Library Authors.
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview This behavior is applied to a text input and it shows a text
* message inside the element if the user hasn't entered any text.
*
* This uses the HTML5 placeholder attribute where it is supported.
*
* This is ported from http://go/labelinput.js
*
* Known issue: Safari does not allow you get to the window object from a
* document. We need that to listen to the onload event. For now we hard code
* the window to the current window.
*
* Known issue: We need to listen to the form submit event but we attach the
* event only once (when created or when it is changed) so if you move the DOM
* node to another form it will not be cleared correctly before submitting.
*
* @see ../demos/labelinput.html
*/
goog.provide('goog.ui.LabelInput');
goog.require('goog.Timer');
goog.require('goog.a11y.aria');
goog.require('goog.a11y.aria.State');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.InputType');
goog.require('goog.dom.TagName');
goog.require('goog.dom.classlist');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventType');
goog.require('goog.ui.Component');
goog.require('goog.userAgent');
goog.requireType('goog.events.BrowserEvent');
goog.requireType('goog.events.Event');
/**
* This creates the label input object.
* @param {string=} opt_label The text to show as the label.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
* @extends {goog.ui.Component}
* @constructor
*/
goog.ui.LabelInput = function(opt_label, opt_domHelper) {
'use strict';
goog.ui.Component.call(this, opt_domHelper);
/**
* The text to show as the label.
* @type {string}
* @private
*/
this.label_ = opt_label || '';
};
goog.inherits(goog.ui.LabelInput, goog.ui.Component);
/**
* Variable used to store the element value on keydown and restore it on
* keypress. See {@link #handleEscapeKeys_}
* @type {?string}
* @private
*/
goog.ui.LabelInput.prototype.ffKeyRestoreValue_ = null;
/**
* The label restore delay after leaving the input.
* @type {number} Delay for restoring the label.
* @protected
*/
goog.ui.LabelInput.prototype.labelRestoreDelayMs = 10;
/** @private {boolean} */
goog.ui.LabelInput.prototype.inFocusAndSelect_;
/** @private {boolean} */
goog.ui.LabelInput.prototype.formAttached_;
/**
* Indicates whether the browser supports the placeholder attribute, new in
* HTML5.
* @type {?boolean}
* @private
*/
goog.ui.LabelInput.supportsPlaceholder_;
/**
* Checks browser support for placeholder attribute.
* @return {boolean} Whether placeholder attribute is supported.
* @private
*/
goog.ui.LabelInput.isPlaceholderSupported_ = function() {
'use strict';
if (goog.ui.LabelInput.supportsPlaceholder_ == null) {
goog.ui.LabelInput.supportsPlaceholder_ =
('placeholder' in goog.dom.createElement(goog.dom.TagName.INPUT));
}
return goog.ui.LabelInput.supportsPlaceholder_;
};
/**
* @type {goog.events.EventHandler}
* @private
*/
goog.ui.LabelInput.prototype.eventHandler_;
/**
* @type {boolean}
* @private
*/
goog.ui.LabelInput.prototype.hasFocus_ = false;
/**
* Creates the DOM nodes needed for the label input.
* @override
*/
goog.ui.LabelInput.prototype.createDom = function() {
'use strict';
this.setElementInternal(this.getDomHelper().createDom(
goog.dom.TagName.INPUT, {'type': goog.dom.InputType.TEXT}));
};
/**
* Decorates an existing HTML input element as a label input. If the element
* has a "label" attribute then that will be used as the label property for the
* label input object.
* @param {Element} element The HTML input element to decorate.
* @override
* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
*/
goog.ui.LabelInput.prototype.decorateInternal = function(element) {
'use strict';
goog.ui.LabelInput.superClass_.decorateInternal.call(this, element);
if (!this.label_) {
this.label_ = element.getAttribute('label') || '';
}
// Check if we're attaching to an element that already has focus.
if (goog.dom.getActiveElement(goog.dom.getOwnerDocument(element)) ==
element) {
this.hasFocus_ = true;
var el = this.getElement();
goog.asserts.assert(el);
goog.dom.classlist.remove(el, this.labelCssClassName);
}
if (goog.ui.LabelInput.isPlaceholderSupported_()) {
this.getElement().placeholder = this.label_;
}
var labelInputElement = this.getElement();
goog.asserts.assert(
labelInputElement, 'The label input element cannot be null.');
goog.a11y.aria.setState(
labelInputElement, goog.a11y.aria.State.LABEL, this.label_);
};
/**
* @override
* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
*/
goog.ui.LabelInput.prototype.enterDocument = function() {
'use strict';
goog.ui.LabelInput.superClass_.enterDocument.call(this);
this.attachEvents_();
this.check_();
// Make it easy for other closure widgets to play nicely with inputs using
// LabelInput:
this.getElement().labelInput_ = this;
};
/**
* @override
* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
*/
goog.ui.LabelInput.prototype.exitDocument = function() {
'use strict';
goog.ui.LabelInput.superClass_.exitDocument.call(this);
this.detachEvents_();
this.getElement().labelInput_ = null;
};
/**
* Attaches the events we need to listen to.
* @private
*/
goog.ui.LabelInput.prototype.attachEvents_ = function() {
'use strict';
var eh = new goog.events.EventHandler(this);
eh.listen(this.getElement(), goog.events.EventType.FOCUS, this.handleFocus_);
eh.listen(this.getElement(), goog.events.EventType.BLUR, this.handleBlur_);
if (goog.ui.LabelInput.isPlaceholderSupported_()) {
this.eventHandler_ = eh;
return;
}
if (goog.userAgent.GECKO) {
eh.listen(
this.getElement(),
[
goog.events.EventType.KEYPRESS, goog.events.EventType.KEYDOWN,
goog.events.EventType.KEYUP
],
this.handleEscapeKeys_);
}
// IE sets defaultValue upon load so we need to test that as well.
var d = goog.dom.getOwnerDocument(this.getElement());
var w = goog.dom.getWindow(d);
eh.listen(w, goog.events.EventType.LOAD, this.handleWindowLoad_);
this.eventHandler_ = eh;
this.attachEventsToForm_();
};
/**
* Adds a listener to the form so that we can clear the input before it is
* submitted.
* @private
* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
*/
goog.ui.LabelInput.prototype.attachEventsToForm_ = function() {
'use strict';
// in case we have are in a form we need to make sure the label is not
// submitted
if (!this.formAttached_ && this.eventHandler_ && this.getElement().form) {
this.eventHandler_.listen(
this.getElement().form, goog.events.EventType.SUBMIT,
this.handleFormSubmit_);
this.formAttached_ = true;
}
};
/**
* Stops listening to the events.
* @private
*/
goog.ui.LabelInput.prototype.detachEvents_ = function() {
'use strict';
if (this.eventHandler_) {
this.eventHandler_.dispose();
this.eventHandler_ = null;
}
};
/** @override */
goog.ui.LabelInput.prototype.disposeInternal = function() {
'use strict';
goog.ui.LabelInput.superClass_.disposeInternal.call(this);
this.detachEvents_();
};
/**
* The CSS class name to add to the input when the user has not entered a
* value.
* @type {string}
*/
goog.ui.LabelInput.prototype.labelCssClassName =
goog.getCssName('label-input-label');
/**
* Handler for the focus event.
* @param {goog.events.Event} e The event object passed in to the event handler.
* @private
*/
goog.ui.LabelInput.prototype.handleFocus_ = function(e) {
'use strict';
this.hasFocus_ = true;
var el = this.getElement();
goog.asserts.assert(el);
goog.dom.classlist.remove(el, this.labelCssClassName);
if (goog.ui.LabelInput.isPlaceholderSupported_()) {
return;
}
if (!this.hasChanged() && !this.inFocusAndSelect_) {
var me = this;
/** @suppress {strictMissingProperties} Part of the go/strict_warnings_migration */
var clearValue = function() {
'use strict';
// Component could be disposed by the time this is called.
if (me.getElement()) {
me.getElement().value = '';
}
};
if (goog.userAgent.IE) {
goog.Timer.callOnce(clearValue, 10);
} else {
clearValue();
}
}
};
/**
* Handler for the blur event.
* @param {goog.events.Event} e The event object passed in to the event handler.
* @private
*/
goog.ui.LabelInput.prototype.handleBlur_ = function(e) {
'use strict';
// We listen to the click event when we enter focusAndSelect mode so we can
// fake an artificial focus when the user clicks on the input box. However,
// if the user clicks on something else (and we lose focus), there is no
// need for an artificial focus event.
if (!goog.ui.LabelInput.isPlaceholderSupported_()) {
this.eventHandler_.unlisten(
this.getElement(), goog.events.EventType.CLICK, this.handleFocus_);
this.ffKeyRestoreValue_ = null;
}
this.hasFocus_ = false;
this.check_();
};
/**
* Handler for key events in Firefox.
*
* If the escape key is pressed when a text input has not been changed manually
* since being focused, the text input will revert to its previous value.
* Firefox does not honor preventDefault for the escape key. The revert happens
* after the keydown event and before every keypress. We therefore store the
* element's value on keydown and restore it on keypress. The restore value is
* nullified on keyup so that {@link #getValue} returns the correct value.
*
* IE and Chrome don't have this problem, Opera blurs in the input box
* completely in a way that preventDefault on the escape key has no effect.
* @param {goog.events.BrowserEvent} e The event object passed in to
* the event handler.
* @private
* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
*/
goog.ui.LabelInput.prototype.handleEscapeKeys_ = function(e) {
'use strict';
if (e.keyCode == 27) {
if (e.type == goog.events.EventType.KEYDOWN) {
this.ffKeyRestoreValue_ = this.getElement().value;
} else if (e.type == goog.events.EventType.KEYPRESS) {
this.getElement().value = /** @type {string} */ (this.ffKeyRestoreValue_);
} else if (e.type == goog.events.EventType.KEYUP) {
this.ffKeyRestoreValue_ = null;
}
e.preventDefault();
}
};
/**
* Handler for the submit event of the form element.
* @param {goog.events.Event} e The event object passed in to the event handler.
* @private
* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
*/
goog.ui.LabelInput.prototype.handleFormSubmit_ = function(e) {
'use strict';
if (!this.hasChanged()) {
this.getElement().value = '';
// allow form to be sent before restoring value
goog.Timer.callOnce(this.handleAfterSubmit_, 10, this);
}
};
/**
* Restore value after submit
* @private
* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
*/
goog.ui.LabelInput.prototype.handleAfterSubmit_ = function() {
'use strict';
if (!this.hasChanged()) {
this.getElement().value = this.label_;
}
};
/**
* Handler for the load event the window. This is needed because
* IE sets defaultValue upon load.
* @param {Event} e The event object passed in to the event handler.
* @private
*/
goog.ui.LabelInput.prototype.handleWindowLoad_ = function(e) {
'use strict';
this.check_();
};
/**
* @return {boolean} Whether the control is currently focused on.
*/
goog.ui.LabelInput.prototype.hasFocus = function() {
'use strict';
return this.hasFocus_;
};
/**
* @return {boolean} Whether the value has been changed by the user.
* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
*/
goog.ui.LabelInput.prototype.hasChanged = function() {
'use strict';
return !!this.getElement() && this.getElement().value != '' &&
this.getElement().value != this.label_;
};
/**
* Clears the value of the input element without resetting the default text.
* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
*/
goog.ui.LabelInput.prototype.clear = function() {
'use strict';
this.getElement().value = '';
// Reset ffKeyRestoreValue_ when non-null
if (this.ffKeyRestoreValue_ != null) {
this.ffKeyRestoreValue_ = '';
}
};
/**
* Clears the value of the input element and resets the default text.
*/
goog.ui.LabelInput.prototype.reset = function() {
'use strict';
if (this.hasChanged()) {
this.clear();
this.check_();
}
};
/**
* Use this to set the value through script to ensure that the label state is
* up to date
* @param {string} s The new value for the input.
* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
*/
goog.ui.LabelInput.prototype.setValue = function(s) {
'use strict';
if (this.ffKeyRestoreValue_ != null) {
this.ffKeyRestoreValue_ = s;
}
this.getElement().value = s;
this.check_();
};
/**
* Returns the current value of the text box, returning an empty string if the
* search box is the default value
* @return {string} The value of the input box.
* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
*/
goog.ui.LabelInput.prototype.getValue = function() {
'use strict';
if (this.ffKeyRestoreValue_ != null) {
// Fix the Firefox from incorrectly reporting the value to calling code
// that attached the listener to keypress before the labelinput
return this.ffKeyRestoreValue_;
}
return this.hasChanged() ? /** @type {string} */ (this.getElement().value) :
'';
};
/**
* Sets the label text as aria-label, and placeholder when supported.
* @param {string} label The text to show as the label.
* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
*/
goog.ui.LabelInput.prototype.setLabel = function(label) {
'use strict';
var labelInputElement = this.getElement();
if (goog.ui.LabelInput.isPlaceholderSupported_()) {
if (labelInputElement) {
labelInputElement.placeholder = label;
}
this.label_ = label;
} else if (!this.hasChanged()) {
// The this.hasChanged() call relies on non-placeholder behavior checking
// prior to setting this.label_ - it also needs to happen prior to the
// this.restoreLabel_() call.
if (labelInputElement) {
labelInputElement.value = '';
}
this.label_ = label;
this.restoreLabel_();
}
// Check if this has been called before DOM structure building
if (labelInputElement) {
goog.a11y.aria.setState(
labelInputElement, goog.a11y.aria.State.LABEL, this.label_);
}
};
/**
* @return {string} The text to show as the label.
*/
goog.ui.LabelInput.prototype.getLabel = function() {
'use strict';
return this.label_;
};
/**
* Checks the state of the input element
* @private
* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
*/
goog.ui.LabelInput.prototype.check_ = function() {
'use strict';
var labelInputElement = this.getElement();
goog.asserts.assert(
labelInputElement, 'The label input element cannot be null.');
if (!goog.ui.LabelInput.isPlaceholderSupported_()) {
// if we haven't got a form yet try now
this.attachEventsToForm_();
} else if (this.getElement().placeholder != this.label_) {
this.getElement().placeholder = this.label_;
}
goog.a11y.aria.setState(
labelInputElement, goog.a11y.aria.State.LABEL, this.label_);
if (!this.hasChanged()) {
if (!this.inFocusAndSelect_ && !this.hasFocus_) {
var el = this.getElement();
goog.asserts.assert(el);
goog.dom.classlist.add(el, this.labelCssClassName);
}
// Allow browser to catchup with CSS changes before restoring the label.
if (!goog.ui.LabelInput.isPlaceholderSupported_()) {
goog.Timer.callOnce(this.restoreLabel_, this.labelRestoreDelayMs, this);
}
} else {
var el = this.getElement();
goog.asserts.assert(el);
goog.dom.classlist.remove(el, this.labelCssClassName);
}
};
/**
* This method focuses the input and selects all the text. If the value hasn't
* changed it will set the value to the label so that the label text is
* selected.
* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
*/
goog.ui.LabelInput.prototype.focusAndSelect = function() {
'use strict';
// We need to check whether the input has changed before focusing
var hc = this.hasChanged();
this.inFocusAndSelect_ = true;
this.getElement().focus();
if (!hc && !goog.ui.LabelInput.isPlaceholderSupported_()) {
this.getElement().value = this.label_;
}
this.getElement().select();
// Since the object now has focus, we won't get a focus event when they
// click in the input element. The expected behavior when you click on
// the default text is that it goes away and allows you to type...so we
// have to fire an artificial focus event when we're in focusAndSelect mode.
if (goog.ui.LabelInput.isPlaceholderSupported_()) {
return;
}
if (this.eventHandler_) {
this.eventHandler_.listenOnce(
this.getElement(), goog.events.EventType.CLICK, this.handleFocus_);
}
// set to false in timer to let IE trigger the focus event
goog.Timer.callOnce(this.focusAndSelect_, 10, this);
};
/**
* Enables/Disables the label input.
* @param {boolean} enabled Whether to enable (true) or disable (false) the
* label input.
* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
*/
goog.ui.LabelInput.prototype.setEnabled = function(enabled) {
'use strict';
this.getElement().disabled = !enabled;
var el = this.getElement();
goog.asserts.assert(el);
goog.dom.classlist.enable(
el, goog.getCssName(this.labelCssClassName, 'disabled'), !enabled);
};
/**
* @return {boolean} True if the label input is enabled, false otherwise.
* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
*/
goog.ui.LabelInput.prototype.isEnabled = function() {
'use strict';
return !this.getElement().disabled;
};
/**
* @private
*/
goog.ui.LabelInput.prototype.focusAndSelect_ = function() {
'use strict';
this.inFocusAndSelect_ = false;
};
/**
* Sets the value of the input element to label.
* @private
* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
*/
goog.ui.LabelInput.prototype.restoreLabel_ = function() {
'use strict';
// Check again in case something changed since this was scheduled.
// We check that the element is still there since this is called by a timer
// and the dispose method may have been called prior to this.
if (this.getElement() && !this.hasChanged() && !this.hasFocus_) {
this.getElement().value = this.label_;
}
};