// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @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.
 *
 * @author arv@google.com (Erik Arvidsson)
 * @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.classlist');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventType');
goog.require('goog.ui.Component');
goog.require('goog.userAgent');



/**
 * 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) {
  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);
goog.tagUnsealableClass(goog.ui.LabelInput);


/**
 * 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.SUPPORTS_PLACEHOLDER_;


/**
 * Checks browser support for placeholder attribute.
 * @return {boolean} Whether placeholder attribute is supported.
 * @private
 */
goog.ui.LabelInput.isPlaceholderSupported_ = function() {
  if (!goog.isDefAndNotNull(goog.ui.LabelInput.SUPPORTS_PLACEHOLDER_)) {
    goog.ui.LabelInput.SUPPORTS_PLACEHOLDER_ = (
        'placeholder' in document.createElement('input'));
  }
  return goog.ui.LabelInput.SUPPORTS_PLACEHOLDER_;
};


/**
 * @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() {
  this.setElementInternal(
      this.getDomHelper().createDom('input', {'type': '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
 */
goog.ui.LabelInput.prototype.decorateInternal = function(element) {
  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 */
goog.ui.LabelInput.prototype.enterDocument = function() {
  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 */
goog.ui.LabelInput.prototype.exitDocument = function() {
  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() {
  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
 */
goog.ui.LabelInput.prototype.attachEventsToForm_ = function() {
  // 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() {
  if (this.eventHandler_) {
    this.eventHandler_.dispose();
    this.eventHandler_ = null;
  }
};


/** @override */
goog.ui.LabelInput.prototype.disposeInternal = function() {
  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.
 */
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) {
  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;
    var clearValue = function() {
      // 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) {
  // 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
 */
goog.ui.LabelInput.prototype.handleEscapeKeys_ = function(e) {
  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
 */
goog.ui.LabelInput.prototype.handleFormSubmit_ = function(e) {
  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
 */
goog.ui.LabelInput.prototype.handleAfterSubmit_ = function() {
  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) {
  this.check_();
};


/**
 * @return {boolean} Whether the control is currently focused on.
 */
goog.ui.LabelInput.prototype.hasFocus = function() {
  return this.hasFocus_;
};


/**
 * @return {boolean} Whether the value has been changed by the user.
 */
goog.ui.LabelInput.prototype.hasChanged = function() {
  return !!this.getElement() && this.getElement().value != '' &&
      this.getElement().value != this.label_;
};


/**
 * Clears the value of the input element without resetting the default text.
 */
goog.ui.LabelInput.prototype.clear = function() {
  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() {
  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.
 */
goog.ui.LabelInput.prototype.setValue = function(s) {
  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.
 */
goog.ui.LabelInput.prototype.getValue = function() {
  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.
 */
goog.ui.LabelInput.prototype.setLabel = function(label) {
  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() {
  return this.label_;
};


/**
 * Checks the state of the input element
 * @private
 */
goog.ui.LabelInput.prototype.check_ = function() {
  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.
 */
goog.ui.LabelInput.prototype.focusAndSelect = function() {
  // 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.
 */
goog.ui.LabelInput.prototype.setEnabled = function(enabled) {
  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.
 */
goog.ui.LabelInput.prototype.isEnabled = function() {
  return !this.getElement().disabled;
};


/**
 * @private
 */
goog.ui.LabelInput.prototype.focusAndSelect_ = function() {
  this.inFocusAndSelect_ = false;
};


/**
 * Sets the value of the input element to label.
 * @private
 */
goog.ui.LabelInput.prototype.restoreLabel_ = function() {
  // 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_;
  }
};
