text: // Copyright 2007 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 Plain text spell checker implementation.
   *
   * @author eae@google.com (Emil A Eklund)
   * @see ../demos/plaintextspellchecker.html
   */
  
  goog.provide('goog.ui.PlainTextSpellChecker');
  
  goog.require('goog.Timer');
  goog.require('goog.a11y.aria');
  goog.require('goog.asserts');
  goog.require('goog.dom');
  goog.require('goog.dom.TagName');
  goog.require('goog.events.EventHandler');
  goog.require('goog.events.EventType');
  goog.require('goog.events.KeyCodes');
  goog.require('goog.events.KeyHandler');
  goog.require('goog.spell.SpellCheck');
  goog.require('goog.style');
  goog.require('goog.ui.AbstractSpellChecker');
  goog.require('goog.ui.Component');
  goog.require('goog.userAgent');
  
  
  
  /**
   * Plain text spell checker implementation.
   *
   * @param {goog.spell.SpellCheck} handler Instance of the SpellCheckHandler
   *     support object to use. A single instance can be shared by multiple
   *     editor components.
   * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
   * @constructor
   * @extends {goog.ui.AbstractSpellChecker}
   * @final
   */
  goog.ui.PlainTextSpellChecker = function(handler, opt_domHelper) {
    goog.ui.AbstractSpellChecker.call(this, handler, opt_domHelper);
  
    /**
     * Correction UI container.
     * @private {!HTMLDivElement}
     */
    this.overlay_ = /** @type {!HTMLDivElement} */
        (this.getDomHelper().createDom(goog.dom.TagName.DIV));
    goog.style.setPreWrap(this.overlay_);
  
    /**
     * Bound async function (to avoid rebinding it on every call).
     * @type {Function}
     * @private
     */
    this.boundContinueAsyncFn_ = goog.bind(this.continueAsync_, this);
  
    /**
     * Regular expression for matching line breaks.
     * @type {RegExp}
     * @private
     */
    this.endOfLineMatcher_ = new RegExp('(.*)(\n|\r\n){0,1}', 'g');
  };
  goog.inherits(goog.ui.PlainTextSpellChecker, goog.ui.AbstractSpellChecker);
  
  
  /**
   * Class name for invalid words.
   * @type {string}
   */
  goog.ui.PlainTextSpellChecker.prototype.invalidWordClassName =
      goog.getCssName('goog-spellcheck-invalidword');
  
  
  /**
   * Class name for corrected words.
   * @type {string}
   */
  goog.ui.PlainTextSpellChecker.prototype.correctedWordClassName =
      goog.getCssName('goog-spellcheck-correctedword');
  
  
  /**
   * Class name for correction pane.
   * @type {string}
   */
  goog.ui.PlainTextSpellChecker.prototype.correctionPaneClassName =
      goog.getCssName('goog-spellcheck-correctionpane');
  
  
  /**
   * Number of words to scan to precharge the dictionary.
   * @type {number}
   * @private
   */
  goog.ui.PlainTextSpellChecker.prototype.dictionaryPreScanSize_ = 1000;
  
  
  /**
   * Size of window. Used to check if a resize operation actually changed the size
   * of the window.
   * @type {goog.math.Size|undefined}
   * @private
   */
  goog.ui.PlainTextSpellChecker.prototype.winSize_;
  
  
  /**
   * Event handler for listening to events without leaking.
   * @type {goog.events.EventHandler|undefined}
   * @private
   */
  goog.ui.PlainTextSpellChecker.prototype.eventHandler_;
  
  
  /**
   * The object handling keyboard events.
   * @type {goog.events.KeyHandler|undefined}
   * @private
   */
  goog.ui.PlainTextSpellChecker.prototype.keyHandler_;
  
  
  /** @private {number} */
  goog.ui.PlainTextSpellChecker.prototype.textArrayIndex_;
  
  
  /** @private {!Array<string>} */
  goog.ui.PlainTextSpellChecker.prototype.textArray_;
  
  
  /** @private {!Array<boolean>} */
  goog.ui.PlainTextSpellChecker.prototype.textArrayProcess_;
  
  
  /**
   * Creates the initial DOM representation for the component.
   * @override
   */
  goog.ui.PlainTextSpellChecker.prototype.createDom = function() {
    this.setElementInternal(
        this.getDomHelper().createElement(goog.dom.TagName.TEXTAREA));
  };
  
  
  /** @override */
  goog.ui.PlainTextSpellChecker.prototype.enterDocument = function() {
    goog.ui.PlainTextSpellChecker.superClass_.enterDocument.call(this);
  
    this.eventHandler_ = new goog.events.EventHandler(this);
    this.keyHandler_ = new goog.events.KeyHandler(this.overlay_);
  
    this.initSuggestionsMenu();
    this.initAccessibility_();
  };
  
  
  /** @override */
  goog.ui.PlainTextSpellChecker.prototype.exitDocument = function() {
    goog.ui.PlainTextSpellChecker.superClass_.exitDocument.call(this);
  
    if (this.eventHandler_) {
      this.eventHandler_.dispose();
      this.eventHandler_ = undefined;
    }
    if (this.keyHandler_) {
      this.keyHandler_.dispose();
      this.keyHandler_ = undefined;
    }
  };
  
  
  /**
   * Initializes suggestions menu. Populates menu with separator and ignore option
   * that are always valid. Suggestions are later added above the separator.
   * @override
   */
  goog.ui.PlainTextSpellChecker.prototype.initSuggestionsMenu = function() {
    goog.ui.PlainTextSpellChecker.superClass_.initSuggestionsMenu.call(this);
    this.eventHandler_.listen(
        /** @type {goog.ui.PopupMenu} */ (this.getMenu()),
        goog.ui.Component.EventType.HIDE, this.onCorrectionHide_);
  };
  
  
  /**
   * Checks spelling for all text and displays correction UI.
   * @override
   */
  goog.ui.PlainTextSpellChecker.prototype.check = function() {
    var text = this.getElement().value;
    this.getElement().readOnly = true;
  
    // Prepare and position correction UI.
    goog.dom.removeChildren(this.overlay_);
    this.overlay_.className = this.correctionPaneClassName;
    if (this.getElement().parentNode != this.overlay_.parentNode) {
      this.getElement().parentNode.appendChild(this.overlay_);
    }
    goog.style.setElementShown(this.overlay_, false);
  
    this.preChargeDictionary_(text);
  };
  
  
  /**
   * Final stage of spell checking - displays the correction UI.
   * @private
   */
  goog.ui.PlainTextSpellChecker.prototype.finishCheck_ = function() {
    // Show correction UI.
    this.positionOverlay_();
    goog.style.setElementShown(this.getElement(), false);
    goog.style.setElementShown(this.overlay_, true);
  
    var eh = this.eventHandler_;
    eh.listen(this.overlay_, goog.events.EventType.CLICK, this.onWordClick_);
    eh.listen(
        /** @type {goog.events.KeyHandler} */ (this.keyHandler_),
        goog.events.KeyHandler.EventType.KEY, this.handleOverlayKeyEvent);
  
    // The position and size of the overlay element needs to be recalculated if
    // the browser window is resized.
    var win = goog.dom.getWindow(this.getDomHelper().getDocument()) || window;
    this.winSize_ = goog.dom.getViewportSize(win);
    eh.listen(win, goog.events.EventType.RESIZE, this.onWindowResize_);
  
    goog.ui.PlainTextSpellChecker.superClass_.check.call(this);
  };
  
  
  /**
   * Start the scan after the dictionary was loaded.
   *
   * @param {string} text text to process.
   * @private
   */
  goog.ui.PlainTextSpellChecker.prototype.preChargeDictionary_ = function(text) {
    this.eventHandler_.listen(
        this.spellCheck, goog.spell.SpellCheck.EventType.READY,
        this.onDictionaryCharged_, true);
  
    this.populateDictionary(text, this.dictionaryPreScanSize_);
  };
  
  
  /**
   * Loads few initial dictionary words into the cache.
   *
   * @param {goog.events.Event} e goog.spell.SpellCheck.EventType.READY event.
   * @private
   */
  goog.ui.PlainTextSpellChecker.prototype.onDictionaryCharged_ = function(e) {
    e.stopPropagation();
    this.eventHandler_.unlisten(
        this.spellCheck, goog.spell.SpellCheck.EventType.READY,
        this.onDictionaryCharged_, true);
    this.checkAsync_(this.getElement().value);
  };
  
  
  /**
   * Processes the included and skips the excluded text ranges.
   * @return {goog.ui.AbstractSpellChecker.AsyncResult} Whether the spell
   *     checking is pending or done.
   * @private
   */
  goog.ui.PlainTextSpellChecker.prototype.spellCheckLoop_ = function() {
    for (var i = this.textArrayIndex_; i < this.textArray_.length; ++i) {
      var text = this.textArray_[i];
      if (this.textArrayProcess_[i]) {
        var result = this.processTextAsync(this.overlay_, text);
        if (result == goog.ui.AbstractSpellChecker.AsyncResult.PENDING) {
          this.textArrayIndex_ = i + 1;
          goog.Timer.callOnce(this.boundContinueAsyncFn_);
          return result;
        }
      } else {
        this.processRange(this.overlay_, text);
      }
    }
  
    this.textArray_ = [];
    this.textArrayProcess_ = [];
  
    return goog.ui.AbstractSpellChecker.AsyncResult.DONE;
  };
  
  
  /**
   * Breaks text into included and excluded ranges using the marker RegExp
   * supplied by the caller.
   *
   * @param {string} text text to process.
   * @private
   */
  goog.ui.PlainTextSpellChecker.prototype.initTextArray_ = function(text) {
    if (!this.excludeMarker) {
      this.textArray_ = [text];
      this.textArrayProcess_ = [true];
      return;
    }
  
    this.textArray_ = [];
    this.textArrayProcess_ = [];
    this.excludeMarker.lastIndex = 0;
    var stringSegmentStart = 0;
    var result;
    while (result = this.excludeMarker.exec(text)) {
      if (result[0].length == 0) {
        break;
      }
      var excludedRange = result[0];
      var includedRange =
          text.substr(stringSegmentStart, result.index - stringSegmentStart);
      if (includedRange) {
        this.textArray_.push(includedRange);
        this.textArrayProcess_.push(true);
      }
      this.textArray_.push(excludedRange);
      this.textArrayProcess_.push(false);
      stringSegmentStart = this.excludeMarker.lastIndex;
    }
  
    var leftoverText = text.substr(stringSegmentStart);
    if (leftoverText) {
      this.textArray_.push(leftoverText);
      this.textArrayProcess_.push(true);
    }
  };
  
  
  /**
   * Starts asynchrnonous spell checking.
   *
   * @param {string} text text to process.
   * @private
   */
  goog.ui.PlainTextSpellChecker.prototype.checkAsync_ = function(text) {
    this.initializeAsyncMode();
    this.initTextArray_(text);
    this.textArrayIndex_ = 0;
    if (this.spellCheckLoop_() ==
        goog.ui.AbstractSpellChecker.AsyncResult.PENDING) {
      return;
    }
    this.finishAsyncProcessing();
    this.finishCheck_();
  };
  
  
  /**
   * Continues asynchrnonous spell checking.
   * @private
   */
  goog.ui.PlainTextSpellChecker.prototype.continueAsync_ = function() {
    // First finish with the current segment.
    var result = this.continueAsyncProcessing();
    if (result == goog.ui.AbstractSpellChecker.AsyncResult.PENDING) {
      goog.Timer.callOnce(this.boundContinueAsyncFn_);
      return;
    }
    if (this.spellCheckLoop_() ==
        goog.ui.AbstractSpellChecker.AsyncResult.PENDING) {
      return;
    }
    this.finishAsyncProcessing();
    this.finishCheck_();
  };
  
  
  /**
   * Processes word.
   *
   * @param {Node} node Node containing word.
   * @param {string} word Word to process.
   * @param {goog.spell.SpellCheck.WordStatus} status Status of word.
   * @override
   */
  goog.ui.PlainTextSpellChecker.prototype.processWord = function(
      node, word, status) {
    node.appendChild(this.createWordElement(word, status));
  };
  
  
  /**
   * Processes range of text - recognized words and separators.
   *
   * @param {Node} node Node containing separator.
   * @param {string} text text to process.
   * @override
   */
  goog.ui.PlainTextSpellChecker.prototype.processRange = function(node, text) {
    this.endOfLineMatcher_.lastIndex = 0;
    var result;
    while (result = this.endOfLineMatcher_.exec(text)) {
      if (result[0].length == 0) {
        break;
      }
      node.appendChild(this.getDomHelper().createTextNode(result[1]));
      if (result[2]) {
        node.appendChild(this.getDomHelper().createElement(goog.dom.TagName.BR));
      }
    }
  };
  
  
  /**
   * Hides correction UI.
   * @override
   */
  goog.ui.PlainTextSpellChecker.prototype.resume = function() {
    var wasVisible = this.isVisible();
  
    goog.ui.PlainTextSpellChecker.superClass_.resume.call(this);
  
    goog.style.setElementShown(this.overlay_, false);
    goog.style.setElementShown(this.getElement(), true);
    this.getElement().readOnly = false;
  
    if (wasVisible) {
      this.getElement().value = goog.dom.getRawTextContent(this.overlay_);
      goog.dom.removeChildren(this.overlay_);
  
      var eh = this.eventHandler_;
      eh.unlisten(this.overlay_, goog.events.EventType.CLICK, this.onWordClick_);
      eh.unlisten(
          /** @type {goog.events.KeyHandler} */ (this.keyHandler_),
          goog.events.KeyHandler.EventType.KEY, this.handleOverlayKeyEvent);
  
      var win = goog.dom.getWindow(this.getDomHelper().getDocument()) || window;
      eh.unlisten(win, goog.events.EventType.RESIZE, this.onWindowResize_);
    }
  };
  
  
  /**
   * Returns desired element properties for the specified status.
   *
   * @param {goog.spell.SpellCheck.WordStatus} status Status of word.
   * @return {!Object} Properties to apply to word element.
   * @override
   */
  goog.ui.PlainTextSpellChecker.prototype.getElementProperties = function(
      status) {
    if (status == goog.spell.SpellCheck.WordStatus.INVALID) {
      return {'class': this.invalidWordClassName};
    } else if (status == goog.spell.SpellCheck.WordStatus.CORRECTED) {
      return {'class': this.correctedWordClassName};
    }
    return {'class': ''};
  };
  
  
  /**
   * Handles the click events.
   *
   * @param {goog.events.BrowserEvent} event Event object.
   * @private
   */
  goog.ui.PlainTextSpellChecker.prototype.onWordClick_ = function(event) {
    if (event.target.className == this.invalidWordClassName ||
        event.target.className == this.correctedWordClassName) {
      this.showSuggestionsMenu(/** @type {!Element} */ (event.target), event);
  
      // Prevent document click handler from closing the menu.
      event.stopPropagation();
    }
  };
  
  
  /**
   * Handles window resize events.
   *
   * @param {goog.events.BrowserEvent} event Event object.
   * @private
   */
  goog.ui.PlainTextSpellChecker.prototype.onWindowResize_ = function(event) {
    var win = goog.dom.getWindow(this.getDomHelper().getDocument()) || window;
    var size = goog.dom.getViewportSize(win);
  
    if (size.width != this.winSize_.width ||
        size.height != this.winSize_.height) {
      goog.style.setElementShown(this.overlay_, false);
      goog.style.setElementShown(this.getElement(), true);
  
      // IE requires a slight delay, allowing the resize operation to take effect.
      if (goog.userAgent.IE) {
        goog.Timer.callOnce(this.resizeOverlay_, 100, this);
      } else {
        this.resizeOverlay_();
      }
      this.winSize_ = size;
    }
  };
  
  
  /**
   * Resizes overlay to match the size of the bound element then displays the
   * overlay. Helper for {@link #onWindowResize_}.
   *
   * @private
   */
  goog.ui.PlainTextSpellChecker.prototype.resizeOverlay_ = function() {
    this.positionOverlay_();
    goog.style.setElementShown(this.getElement(), false);
    goog.style.setElementShown(this.overlay_, true);
  };
  
  
  /**
   * Updates the position and size of the overlay to match the original element.
   *
   * @private
   */
  goog.ui.PlainTextSpellChecker.prototype.positionOverlay_ = function() {
    goog.style.setPosition(
        this.overlay_, goog.style.getPosition(this.getElement()));
    goog.style.setSize(this.overlay_, goog.style.getSize(this.getElement()));
  };
  
  
  /** @override */
  goog.ui.PlainTextSpellChecker.prototype.disposeInternal = function() {
    this.getDomHelper().removeNode(this.overlay_);
    delete this.overlay_;
    delete this.boundContinueAsyncFn_;
    delete this.endOfLineMatcher_;
    goog.ui.PlainTextSpellChecker.superClass_.disposeInternal.call(this);
  };
  
  
  /**
   * Specify ARIA roles and states as appropriate.
   * @private
   */
  goog.ui.PlainTextSpellChecker.prototype.initAccessibility_ = function() {
    goog.asserts.assert(
        this.overlay_,
        'The plain text spell checker DOM element cannot be null.');
    goog.a11y.aria.setRole(this.overlay_, 'region');
    goog.a11y.aria.setState(this.overlay_, 'live', 'assertive');
    this.overlay_.tabIndex = 0;
  
    /** @desc Title for Spell Checker's overlay.*/
    var MSG_SPELLCHECKER_OVERLAY_TITLE = goog.getMsg('Spell Checker');
    this.overlay_.title = MSG_SPELLCHECKER_OVERLAY_TITLE;
  };
  
  
  /**
   * Handles key down for overlay.
   * @param {goog.events.BrowserEvent} e The browser event.
   * @return {boolean} The handled value.
   */
  goog.ui.PlainTextSpellChecker.prototype.handleOverlayKeyEvent = function(e) {
    var handled = false;
    switch (e.keyCode) {
      case goog.events.KeyCodes.RIGHT:
        if (e.ctrlKey) {
          handled = this.navigate(goog.ui.AbstractSpellChecker.Direction.NEXT);
        }
        break;
  
      case goog.events.KeyCodes.LEFT:
        if (e.ctrlKey) {
          handled =
              this.navigate(goog.ui.AbstractSpellChecker.Direction.PREVIOUS);
        }
        break;
  
      case goog.events.KeyCodes.DOWN:
        if (this.getFocusedElementIndex()) {
          var el = this.getDomHelper().getElement(
              this.makeElementId(this.getFocusedElementIndex()));
          if (el) {
            var position = goog.style.getPosition(el);
            var size = goog.style.getSize(el);
            position.x += size.width / 2;
            position.y += size.height / 2;
            this.showSuggestionsMenu(el, position);
            handled = true;
          }
        }
        break;
    }
  
    if (handled) {
      e.preventDefault();
    }
  
    return handled;
  };
  
  
  /**
   * Handles correction menu actions.
   *
   * @param {goog.events.Event} event Action event.
   * @override
   */
  goog.ui.PlainTextSpellChecker.prototype.onCorrectionAction = function(event) {
    goog.ui.PlainTextSpellChecker.superClass_.onCorrectionAction.call(
        this, event);
  
    // In case of editWord base class has already set the focus (on the input),
    // otherwise set the focus back on the word.
    if (event.target != this.getMenuEdit()) {
      this.reFocus_();
    }
  };
  
  
  /**
   * Restores focus when the suggestion menu is hidden.
   *
   * @param {goog.events.BrowserEvent} event Blur event.
   * @private
   */
  goog.ui.PlainTextSpellChecker.prototype.onCorrectionHide_ = function(event) {
    this.reFocus_();
  };
  
  
  /**
   * Sets the focus back on the previously focused word element.
   * @private
   */
  goog.ui.PlainTextSpellChecker.prototype.reFocus_ = function() {
    var el = this.getElementByIndex(this.getFocusedElementIndex());
    if (el) {
      el.focus();
    } else {
      this.overlay_.focus();
    }
  };
  