<html>

<head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta charset="utf-8">
    <title>TextArea Autocomplete</title>
    <script id="sap-ui-bootstrap" src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js" data-sap-ui-theme="sap_belize"
        data-sap-ui-libs="sap.m" data-sap-ui-bindingSyntax="complex" data-sap-ui-compatVersion="edge"
        data-sap-ui-preload="async" data-sap-ui-resourceroots='{
            "com.demo.textarea.autocomplete": "./"
         }'>



        </script>
    <script>
        sap.ui.getCore().attachInit(function () {
            new sap.ui.core.ComponentContainer({
                name: "com.demo.textarea.autocomplete"
            }).placeAt("content");
        });
    </script>
</head>

<body class="sapUiBody" id="content">


</body>

</html>
# SAP UI5 TextArea control with suggestion/autocomplete feature

## Introduction

Enables SAP UI5 [TextArea](https://sapui5.hana.ondemand.com/#/api/sap.m.TextArea) control to support Autocomplete/Suggestion feature

![Demo](/assets/Demo-TextArea.gif "SAP UI5 TextArea with Autocomplete")

## Sample code

## Dependencies

- [jquery-textcomplete](https://github.com/yuku/jquery-textcomplete) plugin

> **_Note_**: Download jquery-textcomplete plugin from this git repo, as it contains some changes(explained below) to support [sap.ui.define](https://sapui5.hana.ondemand.com/#/api/sap.ui/overview) AMD syntax
>
> **_Changes done to jquery-textcomplete plugin file_**:
>
> - Commented lines 2-12
> - Added new line 13
>
> ![Plugin changes](/assets/Pic_02.PNG "jquery.textplugin.js file changes")

## Usage

- Download jquery-textcomplete javascript file from folder _libs/jquery.textcomplete.js_. Call this file from _manifest.json_
- Provide **_id_** for SAP UI5 TextArea XML tag (in this sample _id_ is _'textArea_01'_)
  ![XML View](/assets/Pic_01.PNG "App.view.xml")
- Call the plugin function on _Textarea_ html element only after rendering of the element is done

## How it works

_jquery-textcomplete_ Plugin contains below methods and will be executed in sequence

- **match**: Supports regular expression or function. When user types anything in the UI5 TextArea element it is continuously checked against the regular expression and triggers search when a match is found
- **search**: This takes two input parameters explained below
  - _query_: Regular expression match found in the method _match_ (previous method) of plugin
  - _fnCallback_: Callback method provided by plugin. This method needs to be called after data retrieval is done. _eg_: Get data from backend/model/ajax call etc, then filter data using _query_ and finally call the callback method
- **template**: Template to display results. [HTML list](https://www.w3schools.com/html/html_lists.asp) is displayed to the user. Each record from the method _search_ is passed as input to this method and return the template to be displayed
- **replace**: If user selects an entry from the displayed suggestion list, then while placing the text in _UI5 TextArea_ element the result can be modified. _eg_: In this current sample code countries are shown, when user selects a country from the list it is converted to uppercase and placed in the _UI5 TextArea_ element
{
  "_version": "1.8.0",
  "sap.app": {
    "id": "com.demo.textarea.autocomplete",
    "type": "application",
    "i18n": "i18n/i18n.properties",
    "title": "TextArea Autocomplete",
    "description": "TextArea Autocomplete",
    "applicationVersion": {
      "version": "1.0.0"
    }
  },
  "sap.ui": {
    "technology": "UI5",
    "deviceTypes": {
      "desktop": true,
      "tablet": true,
      "phone": true
    }
  },
  "sap.ui5": {
    "rootView": {
      "viewName": "com.demo.textarea.autocomplete.view.App",
      "type": "XML",
      "async": true,
      "id": "app"
    },
    "dependencies": {
      "minUI5Version": "1.30",
      "libs": {
        "sap.m": {},
        "sap.viz": {}
      }
    },
    "models": {
      "countriesData": {
        "type": "sap.ui.model.json.JSONModel",
        "uri": "model/countries.json"
      }
    },
    "resources": {
      "js": [{ "uri": "libs/jquery.textcomplete.js" }],
      "css": [{ "uri": "styles/autocomplete.css" }]
    }
  }
}
sap.ui.define(
    ["sap/ui/core/UIComponent", "sap/ui/model/json/JSONModel"],
    function(UIComponent, JSONModel) {
      "use strict";
      return UIComponent.extend("com.demo.textarea.autocomplete.Component", {
        metadata: {
          manifest: "json"
        },
        init: function() {
          // call the init function of the parent
          UIComponent.prototype.init.apply(this, arguments);
        }
      });
    }
  );
sap.ui.define(["sap/ui/core/mvc/Controller"], function(Controller) {
  "use strict";

  return Controller.extend("com.demo.textarea.autocomplete.controller.App", {
    onInit: function() {
      var that = this;

      this.getView().byId("textArea_01").onAfterRendering = function() {
        // call autocomplete plugin function after rendering of textarea is completed
        that.enableAutoComplete();
      };
    },

    onBeforeRendering: function() {},

    onAfterRendering: function() {},

    enableAutoComplete: function() {
      var that = this;
      var oControl = this.getView().byId("textArea_01");

      // get textarea htmltag from UI5 control
      var jQueryTextArea = jQuery("#" + oControl.getId()).find("textarea");

      jQueryTextArea.textcomplete([
        {
          // #1 - Regular experession used to trigger search
          match: /(\b(\w+))$/, // --> triggers search for every char typed

          // #2 - Function called at every new key stroke
          search: function(query, fnCallback) {
            var pData = Promise.resolve(
              // jQuery.ajax({
              //   url: "./model/countries.json",
              //   method: "GET"
              // })

              that
                .getOwnerComponent()
                .getModel("countriesData")
                .getData()
            );

            pData.then(function(oResult) {
              fnCallback(
                oResult.data.filter(function(oRecord) {
                  // filter results based on query
                  return oRecord.name
                    .toUpperCase()
                    .startsWith(query.toUpperCase());
                })
              );
            });
          },

          // #3 - Template used to display each result (also supports HTML tags)
          template: function(hit) {
            // Returns the highlighted version of the name attribute
            return hit.name;
          },

          // #4 - Template used to display the selected result in the textarea
          replace: function(hit) {
            return hit.name.toUpperCase();
          }
        }
      ]);
    }
  });
});
(function(factory) {
  // if (typeof define === "function" && define.amd) {
  //   // AMD. Register as an anonymous module.
  //   define(["jquery"], factory);
  // } else if (typeof module === "object" && module.exports) {
  //   var $ = require("jquery");
  //   module.exports = factory($);
  // } else {
  //   // Browser globals
  //   factory(jQuery);
  // }

  factory(jQuery); // Added for UI5
})(function(jQuery) {
  /*!
   * jQuery.textcomplete
   *
   * Repository: https://github.com/yuku-t/jquery-textcomplete
   * License:    MIT (https://github.com/yuku-t/jquery-textcomplete/blob/master/LICENSE)
   * Author:     Yuku Takahashi
   */

  if (typeof jQuery === "undefined") {
    throw new Error("jQuery.textcomplete requires jQuery");
  }

  +(function($) {
    "use strict";

    var warn = function(message) {
      if (console.warn) {
        console.warn(message);
      }
    };

    var id = 1;

    $.fn.textcomplete = function(strategies, option) {
      var args = Array.prototype.slice.call(arguments);
      return this.each(function() {
        var self = this;
        var $this = $(this);
        var completer = $this.data("textComplete");
        if (!completer) {
          option || (option = {});
          option._oid = id++; // unique object id
          completer = new $.fn.textcomplete.Completer(this, option);
          $this.data("textComplete", completer);
        }
        if (typeof strategies === "string") {
          if (!completer) return;
          args.shift();
          completer[strategies].apply(completer, args);
          if (strategies === "destroy") {
            $this.removeData("textComplete");
          }
        } else {
          // For backward compatibility.
          // TODO: Remove at v0.4
          $.each(strategies, function(obj) {
            $.each(["header", "footer", "placement", "maxCount"], function(
              name
            ) {
              if (obj[name]) {
                completer.option[name] = obj[name];
                warn(name + "as a strategy param is deprecated. Use option.");
                delete obj[name];
              }
            });
          });
          completer.register(
            $.fn.textcomplete.Strategy.parse(strategies, {
              el: self,
              $el: $this
            })
          );
        }
      });
    };
  })(jQuery);

  +(function($) {
    "use strict";

    // Exclusive execution control utility.
    //
    // func - The function to be locked. It is executed with a function named
    //        `free` as the first argument. Once it is called, additional
    //        execution are ignored until the free is invoked. Then the last
    //        ignored execution will be replayed immediately.
    //
    // Examples
    //
    //   var lockedFunc = lock(function (free) {
    //     setTimeout(function { free(); }, 1000); // It will be free in 1 sec.
    //     console.log('Hello, world');
    //   });
    //   lockedFunc();  // => 'Hello, world'
    //   lockedFunc();  // none
    //   lockedFunc();  // none
    //   // 1 sec past then
    //   // => 'Hello, world'
    //   lockedFunc();  // => 'Hello, world'
    //   lockedFunc();  // none
    //
    // Returns a wrapped function.
    var lock = function(func) {
      var locked, queuedArgsToReplay;

      return function() {
        // Convert arguments into a real array.
        var args = Array.prototype.slice.call(arguments);
        if (locked) {
          // Keep a copy of this argument list to replay later.
          // OK to overwrite a previous value because we only replay
          // the last one.
          queuedArgsToReplay = args;
          return;
        }
        locked = true;
        var self = this;
        args.unshift(function replayOrFree() {
          if (queuedArgsToReplay) {
            // Other request(s) arrived while we were locked.
            // Now that the lock is becoming available, replay
            // the latest such request, then call back here to
            // unlock (or replay another request that arrived
            // while this one was in flight).
            var replayArgs = queuedArgsToReplay;
            queuedArgsToReplay = undefined;
            replayArgs.unshift(replayOrFree);
            func.apply(self, replayArgs);
          } else {
            locked = false;
          }
        });
        func.apply(this, args);
      };
    };

    var isString = function(obj) {
      return Object.prototype.toString.call(obj) === "[object String]";
    };

    var uniqueId = 0;
    var initializedEditors = [];

    function Completer(element, option) {
      this.$el = $(element);
      this.id = "textcomplete" + uniqueId++;
      this.strategies = [];
      this.views = [];
      this.option = $.extend({}, Completer.defaults, option);

      if (
        !this.$el.is("input[type=text]") &&
        !this.$el.is("input[type=search]") &&
        !this.$el.is("textarea") &&
        !element.isContentEditable &&
        element.contentEditable != "true"
      ) {
        throw new Error(
          "textcomplete must be called on a Textarea or a ContentEditable."
        );
      }

      // use ownerDocument to fix iframe / IE issues
      if (element === element.ownerDocument.activeElement) {
        // element has already been focused. Initialize view objects immediately.
        this.initialize();
      } else {
        // Initialize view objects lazily.
        var self = this;
        this.$el.one("focus." + this.id, function() {
          self.initialize();
        });

        // Special handling for CKEditor: lazy init on instance load
        if (
          (!this.option.adapter || this.option.adapter == "CKEditor") &&
          typeof CKEDITOR != "undefined" &&
          this.$el.is("textarea")
        ) {
          CKEDITOR.on("instanceReady", function(event) {
            //For multiple ckeditors on one page: this needs to be executed each time a ckeditor-instance is ready.

            if ($.inArray(event.editor.id, initializedEditors) == -1) {
              //For multiple ckeditors on one page: focus-eventhandler should only be added once for every editor.
              initializedEditors.push(event.editor.id);

              event.editor.on("focus", function(event2) {
                //replace the element with the Iframe element and flag it as CKEditor
                self.$el = $(event.editor.editable().$);
                if (!self.option.adapter) {
                  self.option.adapter = $.fn.textcomplete["CKEditor"];
                }
                self.option.ckeditor_instance = event.editor; //For multiple ckeditors on one page: in the old code this was not executed when adapter was alread set. So we were ALWAYS working with the FIRST instance.
                self.initialize();
              });
            }
          });
        }
      }
    }

    Completer.defaults = {
      appendTo: "body",
      className: "", // deprecated option
      dropdownClassName: "dropdown-menu textcomplete-dropdown",
      maxCount: 10,
      zIndex: "100",
      rightEdgeOffset: 30
    };

    $.extend(Completer.prototype, {
      // Public properties
      // -----------------

      id: null,
      option: null,
      strategies: null,
      adapter: null,
      dropdown: null,
      $el: null,
      $iframe: null,

      // Public methods
      // --------------

      initialize: function() {
        var element = this.$el.get(0);

        // check if we are in an iframe
        // we need to alter positioning logic if using an iframe
        if (
          this.$el.prop("ownerDocument") !== document &&
          window.frames.length
        ) {
          for (
            var iframeIndex = 0;
            iframeIndex < window.frames.length;
            iframeIndex++
          ) {
            if (
              this.$el.prop("ownerDocument") ===
              window.frames[iframeIndex].document
            ) {
              this.$iframe = $(window.frames[iframeIndex].frameElement);
              break;
            }
          }
        }

        // Initialize view objects.
        this.dropdown = new $.fn.textcomplete.Dropdown(
          element,
          this,
          this.option
        );
        var Adapter, viewName;
        if (this.option.adapter) {
          Adapter = this.option.adapter;
        } else {
          if (
            this.$el.is("textarea") ||
            this.$el.is("input[type=text]") ||
            this.$el.is("input[type=search]")
          ) {
            viewName =
              typeof element.selectionEnd === "number"
                ? "Textarea"
                : "IETextarea";
          } else {
            viewName = "ContentEditable";
          }
          Adapter = $.fn.textcomplete[viewName];
        }
        this.adapter = new Adapter(element, this, this.option);
      },

      destroy: function() {
        this.$el.off("." + this.id);
        if (this.adapter) {
          this.adapter.destroy();
        }
        if (this.dropdown) {
          this.dropdown.destroy();
        }
        this.$el = this.adapter = this.dropdown = null;
      },

      deactivate: function() {
        if (this.dropdown) {
          this.dropdown.deactivate();
        }
      },

      // Invoke textcomplete.
      trigger: function(text, skipUnchangedTerm) {
        if (!this.dropdown) {
          this.initialize();
        }
        text != null || (text = this.adapter.getTextFromHeadToCaret());
        var searchQuery = this._extractSearchQuery(text);
        if (searchQuery.length) {
          var term = searchQuery[1];
          // Ignore shift-key, ctrl-key and so on.
          if (skipUnchangedTerm && this._term === term && term !== "") {
            return;
          }
          this._term = term;
          this._search.apply(this, searchQuery);
        } else {
          this._term = null;
          this.dropdown.deactivate();
        }
      },

      fire: function(eventName) {
        var args = Array.prototype.slice.call(arguments, 1);
        this.$el.trigger(eventName, args);
        return this;
      },

      register: function(strategies) {
        Array.prototype.push.apply(this.strategies, strategies);
      },

      // Insert the value into adapter view. It is called when the dropdown is clicked
      // or selected.
      //
      // value    - The selected element of the array callbacked from search func.
      // strategy - The Strategy object.
      // e        - Click or keydown event object.
      select: function(value, strategy, e) {
        this._term = null;
        this.adapter.select(value, strategy, e);
        this.fire("change").fire("textComplete:select", value, strategy);
        this.adapter.focus();
      },

      // Private properties
      // ------------------

      _clearAtNext: true,
      _term: null,

      // Private methods
      // ---------------

      // Parse the given text and extract the first matching strategy.
      //
      // Returns an array including the strategy, the query term and the match
      // object if the text matches an strategy; otherwise returns an empty array.
      _extractSearchQuery: function(text) {
        for (var i = 0; i < this.strategies.length; i++) {
          var strategy = this.strategies[i];
          var context = strategy.context(text);
          if (context || context === "") {
            var matchRegexp = $.isFunction(strategy.match)
              ? strategy.match(text)
              : strategy.match;
            if (isString(context)) {
              text = context;
            }
            var match = text.match(matchRegexp);
            if (match) {
              return [strategy, match[strategy.index], match];
            }
          }
        }
        return [];
      },

      // Call the search method of selected strategy..
      _search: lock(function(free, strategy, term, match) {
        var self = this;
        strategy.search(
          term,
          function(data, stillSearching) {
            if (!self.dropdown.shown) {
              self.dropdown.activate();
            }
            if (self._clearAtNext) {
              // The first callback in the current lock.
              self.dropdown.clear();
              self._clearAtNext = false;
            }
            self.dropdown.setPosition(self.adapter.getCaretPosition());
            self.dropdown.render(self._zip(data, strategy, term));
            if (!stillSearching) {
              // The last callback in the current lock.
              free();
              self._clearAtNext = true; // Call dropdown.clear at the next time.
            }
          },
          match
        );
      }),

      // Build a parameter for Dropdown#render.
      //
      // Examples
      //
      //  this._zip(['a', 'b'], 's');
      //  //=> [{ value: 'a', strategy: 's' }, { value: 'b', strategy: 's' }]
      _zip: function(data, strategy, term) {
        return $.map(data, function(value) {
          return { value: value, strategy: strategy, term: term };
        });
      }
    });

    $.fn.textcomplete.Completer = Completer;
  })(jQuery);

  +(function($) {
    "use strict";

    var $window = $(window);

    var include = function(zippedData, datum) {
      var i, elem;
      var idProperty = datum.strategy.idProperty;
      for (i = 0; i < zippedData.length; i++) {
        elem = zippedData[i];
        if (elem.strategy !== datum.strategy) continue;
        if (idProperty) {
          if (elem.value[idProperty] === datum.value[idProperty]) return true;
        } else {
          if (elem.value === datum.value) return true;
        }
      }
      return false;
    };

    var dropdownViews = {};
    $(document).on("click", function(e) {
      var id = e.originalEvent && e.originalEvent.keepTextCompleteDropdown;
      $.each(dropdownViews, function(key, view) {
        if (key !== id) {
          view.deactivate();
        }
      });
    });

    var commands = {
      SKIP_DEFAULT: 0,
      KEY_UP: 1,
      KEY_DOWN: 2,
      KEY_ENTER: 3,
      KEY_PAGEUP: 4,
      KEY_PAGEDOWN: 5,
      KEY_ESCAPE: 6
    };

    // Dropdown view
    // =============

    // Construct Dropdown object.
    //
    // element - Textarea or contenteditable element.
    function Dropdown(element, completer, option) {
      this.$el = Dropdown.createElement(option);
      this.completer = completer;
      this.id = completer.id + "dropdown";
      this._data = []; // zipped data.
      this.$inputEl = $(element);
      this.option = option;

      // Override setPosition method.
      if (option.listPosition) {
        this.setPosition = option.listPosition;
      }
      if (option.height) {
        this.$el.height(option.height);
      }
      var self = this;
      $.each(
        [
          "maxCount",
          "placement",
          "footer",
          "header",
          "noResultsMessage",
          "className"
        ],
        function(_i, name) {
          if (option[name] != null) {
            self[name] = option[name];
          }
        }
      );
      this._bindEvents(element);
      dropdownViews[this.id] = this;
    }

    $.extend(Dropdown, {
      // Class methods
      // -------------

      createElement: function(option) {
        var $parent = option.appendTo;
        if (!($parent instanceof $)) {
          $parent = $($parent);
        }
        var $el = $("<ul></ul>")
          .addClass(option.dropdownClassName)
          .attr("id", "textcomplete-dropdown-" + option._oid)
          .css({
            display: "none",
            left: 0,
            position: "absolute",
            zIndex: option.zIndex
          })
          .appendTo($parent);
        return $el;
      }
    });

    $.extend(Dropdown.prototype, {
      // Public properties
      // -----------------

      $el: null, // jQuery object of ul.dropdown-menu element.
      $inputEl: null, // jQuery object of target textarea.
      completer: null,
      footer: null,
      header: null,
      id: null,
      maxCount: null,
      placement: "",
      shown: false,
      data: [], // Shown zipped data.
      className: "",

      // Public methods
      // --------------

      destroy: function() {
        // Don't remove $el because it may be shared by several textcompletes.
        this.deactivate();

        this.$el.off("." + this.id);
        this.$inputEl.off("." + this.id);
        this.clear();
        this.$el.remove();
        this.$el = this.$inputEl = this.completer = null;
        delete dropdownViews[this.id];
      },

      render: function(zippedData) {
        var contentsHtml = this._buildContents(zippedData);
        var unzippedData = $.map(zippedData, function(d) {
          return d.value;
        });
        if (zippedData.length) {
          var strategy = zippedData[0].strategy;
          if (strategy.id) {
            this.$el.attr("data-strategy", strategy.id);
          } else {
            this.$el.removeAttr("data-strategy");
          }
          this._renderHeader(unzippedData);
          this._renderFooter(unzippedData);
          if (contentsHtml) {
            this._renderContents(contentsHtml);
            this._fitToBottom();
            this._fitToRight();
            this._activateIndexedItem();
          }
          this._setScroll();
        } else if (this.noResultsMessage) {
          this._renderNoResultsMessage(unzippedData);
        } else if (this.shown) {
          this.deactivate();
        }
      },

      setPosition: function(pos) {
        // Make the dropdown fixed if the input is also fixed
        // This can't be done during init, as textcomplete may be used on multiple elements on the same page
        // Because the same dropdown is reused behind the scenes, we need to recheck every time the dropdown is showed
        var position = "absolute";
        // Check if input or one of its parents has positioning we need to care about
        this.$inputEl.add(this.$inputEl.parents()).each(function() {
          if ($(this).css("position") === "absolute")
            // The element has absolute positioning, so it's all OK
            return false;
          if ($(this).css("position") === "fixed") {
            pos.top -= $window.scrollTop();
            pos.left -= $window.scrollLeft();
            position = "fixed";
            return false;
          }
        });
        this.$el.css(this._applyPlacement(pos));
        this.$el.css({ position: position }); // Update positioning

        return this;
      },

      clear: function() {
        this.$el.html("");
        this.data = [];
        this._index = 0;
        this._$header = this._$footer = this._$noResultsMessage = null;
      },

      activate: function() {
        if (!this.shown) {
          this.clear();
          this.$el.show();
          if (this.className) {
            this.$el.addClass(this.className);
          }
          this.completer.fire("textComplete:show");
          this.shown = true;
        }
        return this;
      },

      deactivate: function() {
        if (this.shown) {
          this.$el.hide();
          if (this.className) {
            this.$el.removeClass(this.className);
          }
          this.completer.fire("textComplete:hide");
          this.shown = false;
        }
        return this;
      },

      isUp: function(e) {
        return e.keyCode === 38 || (e.ctrlKey && e.keyCode === 80); // UP, Ctrl-P
      },

      isDown: function(e) {
        return e.keyCode === 40 || (e.ctrlKey && e.keyCode === 78); // DOWN, Ctrl-N
      },

      isEnter: function(e) {
        var modifiers = e.ctrlKey || e.altKey || e.metaKey || e.shiftKey;
        return (
          !modifiers &&
          (e.keyCode === 13 ||
            e.keyCode === 9 ||
            (this.option.completeOnSpace === true && e.keyCode === 32))
        ); // ENTER, TAB
      },

      isPageup: function(e) {
        return e.keyCode === 33; // PAGEUP
      },

      isPagedown: function(e) {
        return e.keyCode === 34; // PAGEDOWN
      },

      isEscape: function(e) {
        return e.keyCode === 27; // ESCAPE
      },

      // Private properties
      // ------------------

      _data: null, // Currently shown zipped data.
      _index: null,
      _$header: null,
      _$noResultsMessage: null,
      _$footer: null,

      // Private methods
      // ---------------

      _bindEvents: function() {
        this.$el.on(
          "mousedown." + this.id,
          ".textcomplete-item",
          $.proxy(this._onClick, this)
        );
        this.$el.on(
          "touchstart." + this.id,
          ".textcomplete-item",
          $.proxy(this._onClick, this)
        );
        this.$el.on(
          "mouseover." + this.id,
          ".textcomplete-item",
          $.proxy(this._onMouseover, this)
        );
        this.$inputEl.on("keydown." + this.id, $.proxy(this._onKeydown, this));
      },

      _onClick: function(e) {
        var $el = $(e.target);
        e.preventDefault();
        e.originalEvent.keepTextCompleteDropdown = this.id;
        if (!$el.hasClass("textcomplete-item")) {
          $el = $el.closest(".textcomplete-item");
        }
        var datum = this.data[parseInt($el.data("index"), 10)];
        this.completer.select(datum.value, datum.strategy, e);
        var self = this;
        // Deactive at next tick to allow other event handlers to know whether
        // the dropdown has been shown or not.
        setTimeout(function() {
          self.deactivate();
          if (e.type === "touchstart") {
            self.$inputEl.focus();
          }
        }, 0);
      },

      // Activate hovered item.
      _onMouseover: function(e) {
        var $el = $(e.target);
        e.preventDefault();
        if (!$el.hasClass("textcomplete-item")) {
          $el = $el.closest(".textcomplete-item");
        }
        this._index = parseInt($el.data("index"), 10);
        this._activateIndexedItem();
      },

      _onKeydown: function(e) {
        if (!this.shown) {
          return;
        }

        var command;

        if ($.isFunction(this.option.onKeydown)) {
          command = this.option.onKeydown(e, commands);
        }

        if (command == null) {
          command = this._defaultKeydown(e);
        }

        switch (command) {
          case commands.KEY_UP:
            e.preventDefault();
            this._up();
            break;
          case commands.KEY_DOWN:
            e.preventDefault();
            this._down();
            break;
          case commands.KEY_ENTER:
            e.preventDefault();
            this._enter(e);
            break;
          case commands.KEY_PAGEUP:
            e.preventDefault();
            this._pageup();
            break;
          case commands.KEY_PAGEDOWN:
            e.preventDefault();
            this._pagedown();
            break;
          case commands.KEY_ESCAPE:
            e.preventDefault();
            this.deactivate();
            break;
        }
      },

      _defaultKeydown: function(e) {
        if (this.isUp(e)) {
          return commands.KEY_UP;
        } else if (this.isDown(e)) {
          return commands.KEY_DOWN;
        } else if (this.isEnter(e)) {
          return commands.KEY_ENTER;
        } else if (this.isPageup(e)) {
          return commands.KEY_PAGEUP;
        } else if (this.isPagedown(e)) {
          return commands.KEY_PAGEDOWN;
        } else if (this.isEscape(e)) {
          return commands.KEY_ESCAPE;
        }
      },

      _up: function() {
        if (this._index === 0) {
          this._index = this.data.length - 1;
        } else {
          this._index -= 1;
        }
        this._activateIndexedItem();
        this._setScroll();
      },

      _down: function() {
        if (this._index === this.data.length - 1) {
          this._index = 0;
        } else {
          this._index += 1;
        }
        this._activateIndexedItem();
        this._setScroll();
      },

      _enter: function(e) {
        var datum = this.data[
          parseInt(this._getActiveElement().data("index"), 10)
        ];
        this.completer.select(datum.value, datum.strategy, e);
        this.deactivate();
      },

      _pageup: function() {
        var target = 0;
        var threshold =
          this._getActiveElement().position().top - this.$el.innerHeight();
        this.$el.children().each(function(i) {
          if ($(this).position().top + $(this).outerHeight() > threshold) {
            target = i;
            return false;
          }
        });
        this._index = target;
        this._activateIndexedItem();
        this._setScroll();
      },

      _pagedown: function() {
        var target = this.data.length - 1;
        var threshold =
          this._getActiveElement().position().top + this.$el.innerHeight();
        this.$el.children().each(function(i) {
          if ($(this).position().top > threshold) {
            target = i;
            return false;
          }
        });
        this._index = target;
        this._activateIndexedItem();
        this._setScroll();
      },

      _activateIndexedItem: function() {
        this.$el.find(".textcomplete-item.active").removeClass("active");
        this._getActiveElement().addClass("active");
      },

      _getActiveElement: function() {
        return this.$el.children(".textcomplete-item:nth(" + this._index + ")");
      },

      _setScroll: function() {
        var $activeEl = this._getActiveElement();
        var itemTop = $activeEl.position().top;
        var itemHeight = $activeEl.outerHeight();
        var visibleHeight = this.$el.innerHeight();
        var visibleTop = this.$el.scrollTop();
        if (
          this._index === 0 ||
          this._index == this.data.length - 1 ||
          itemTop < 0
        ) {
          this.$el.scrollTop(itemTop + visibleTop);
        } else if (itemTop + itemHeight > visibleHeight) {
          this.$el.scrollTop(itemTop + itemHeight + visibleTop - visibleHeight);
        }
      },

      _buildContents: function(zippedData) {
        var datum, i, index;
        var html = "";
        for (i = 0; i < zippedData.length; i++) {
          if (this.data.length === this.maxCount) break;
          datum = zippedData[i];
          if (include(this.data, datum)) {
            continue;
          }
          index = this.data.length;
          this.data.push(datum);
          html +=
            '<li class="textcomplete-item" data-index="' + index + '"><a>';
          html += datum.strategy.template(datum.value, datum.term);
          html += "</a></li>";
        }
        return html;
      },

      _renderHeader: function(unzippedData) {
        if (this.header) {
          if (!this._$header) {
            this._$header = $(
              '<li class="textcomplete-header"></li>'
            ).prependTo(this.$el);
          }
          var html = $.isFunction(this.header)
            ? this.header(unzippedData)
            : this.header;
          this._$header.html(html);
        }
      },

      _renderFooter: function(unzippedData) {
        if (this.footer) {
          if (!this._$footer) {
            this._$footer = $('<li class="textcomplete-footer"></li>').appendTo(
              this.$el
            );
          }
          var html = $.isFunction(this.footer)
            ? this.footer(unzippedData)
            : this.footer;
          this._$footer.html(html);
        }
      },

      _renderNoResultsMessage: function(unzippedData) {
        if (this.noResultsMessage) {
          if (!this._$noResultsMessage) {
            this._$noResultsMessage = $(
              '<li class="textcomplete-no-results-message"></li>'
            ).appendTo(this.$el);
          }
          var html = $.isFunction(this.noResultsMessage)
            ? this.noResultsMessage(unzippedData)
            : this.noResultsMessage;
          this._$noResultsMessage.html(html);
        }
      },

      _renderContents: function(html) {
        if (this._$footer) {
          this._$footer.before(html);
        } else {
          this.$el.append(html);
        }
      },

      _fitToBottom: function() {
        var windowScrollBottom = $window.scrollTop() + $window.height();
        var height = this.$el.height();
        if (this.$el.position().top + height > windowScrollBottom) {
          // only do this if we are not in an iframe
          if (!this.completer.$iframe) {
            this.$el.offset({ top: windowScrollBottom - height });
          }
        }
      },

      _fitToRight: function() {
        // We don't know how wide our content is until the browser positions us, and at that point it clips us
        // to the document width so we don't know if we would have overrun it. As a heuristic to avoid that clipping
        // (which makes our elements wrap onto the next line and corrupt the next item), if we're close to the right
        // edge, move left. We don't know how far to move left, so just keep nudging a bit.
        var tolerance = this.option.rightEdgeOffset; // pixels. Make wider than vertical scrollbar because we might not be able to use that space.
        var lastOffset = this.$el.offset().left,
          offset;
        var width = this.$el.width();
        var maxLeft = $window.width() - tolerance;
        while (lastOffset + width > maxLeft) {
          this.$el.offset({ left: lastOffset - tolerance });
          offset = this.$el.offset().left;
          if (offset >= lastOffset) {
            break;
          }
          lastOffset = offset;
        }
      },

      _applyPlacement: function(position) {
        // If the 'placement' option set to 'top', move the position above the element.
        if (this.placement.indexOf("top") !== -1) {
          // Overwrite the position object to set the 'bottom' property instead of the top.
          position = {
            top: "auto",
            bottom:
              this.$el.parent().height() - position.top + position.lineHeight,
            left: position.left
          };
        } else {
          position.bottom = "auto";
          delete position.lineHeight;
        }
        if (this.placement.indexOf("absleft") !== -1) {
          position.left = 0;
        } else if (this.placement.indexOf("absright") !== -1) {
          position.right = 0;
          position.left = "auto";
        }
        return position;
      }
    });

    $.fn.textcomplete.Dropdown = Dropdown;
    $.extend($.fn.textcomplete, commands);
  })(jQuery);

  +(function($) {
    "use strict";

    // Memoize a search function.
    var memoize = function(func) {
      var memo = {};
      return function(term, callback) {
        if (memo[term]) {
          callback(memo[term]);
        } else {
          func.call(this, term, function(data) {
            memo[term] = (memo[term] || []).concat(data);
            callback.apply(null, arguments);
          });
        }
      };
    };

    function Strategy(options) {
      $.extend(this, options);
      if (this.cache) {
        this.search = memoize(this.search);
      }
    }

    Strategy.parse = function(strategiesArray, params) {
      return $.map(strategiesArray, function(strategy) {
        var strategyObj = new Strategy(strategy);
        strategyObj.el = params.el;
        strategyObj.$el = params.$el;
        return strategyObj;
      });
    };

    $.extend(Strategy.prototype, {
      // Public properties
      // -----------------

      // Required
      match: null,
      replace: null,
      search: null,

      // Optional
      id: null,
      cache: false,
      context: function() {
        return true;
      },
      index: 2,
      template: function(obj) {
        return obj;
      },
      idProperty: null
    });

    $.fn.textcomplete.Strategy = Strategy;
  })(jQuery);

  +(function($) {
    "use strict";

    var now =
      Date.now ||
      function() {
        return new Date().getTime();
      };

    // Returns a function, that, as long as it continues to be invoked, will not
    // be triggered. The function will be called after it stops being called for
    // `wait` msec.
    //
    // This utility function was originally implemented at Underscore.js.
    var debounce = function(func, wait) {
      var timeout, args, context, timestamp, result;
      var later = function() {
        var last = now() - timestamp;
        if (last < wait) {
          timeout = setTimeout(later, wait - last);
        } else {
          timeout = null;
          result = func.apply(context, args);
          context = args = null;
        }
      };

      return function() {
        context = this;
        args = arguments;
        timestamp = now();
        if (!timeout) {
          timeout = setTimeout(later, wait);
        }
        return result;
      };
    };

    function Adapter() {}

    $.extend(Adapter.prototype, {
      // Public properties
      // -----------------

      id: null, // Identity.
      completer: null, // Completer object which creates it.
      el: null, // Textarea element.
      $el: null, // jQuery object of the textarea.
      option: null,

      // Public methods
      // --------------

      initialize: function(element, completer, option) {
        this.el = element;
        this.$el = $(element);
        this.id = completer.id + this.constructor.name;
        this.completer = completer;
        this.option = option;

        if (this.option.debounce) {
          this._onKeyup = debounce(this._onKeyup, this.option.debounce);
        }

        this._bindEvents();
      },

      destroy: function() {
        this.$el.off("." + this.id); // Remove all event handlers.
        this.$el = this.el = this.completer = null;
      },

      // Update the element with the given value and strategy.
      //
      // value    - The selected object. It is one of the item of the array
      //            which was callbacked from the search function.
      // strategy - The Strategy associated with the selected value.
      select: function(/* value, strategy */) {
        throw new Error("Not implemented");
      },

      // Returns the caret's relative coordinates from body's left top corner.
      getCaretPosition: function() {
        var position = this._getCaretRelativePosition();
        var offset = this.$el.offset();

        // Calculate the left top corner of `this.option.appendTo` element.
        var $parent = this.option.appendTo;
        if ($parent) {
          if (!($parent instanceof $)) {
            $parent = $($parent);
          }
          var parentOffset = $parent.offsetParent().offset();
          offset.top -= parentOffset.top;
          offset.left -= parentOffset.left;
        }

        position.top += offset.top;
        position.left += offset.left;
        return position;
      },

      // Focus on the element.
      focus: function() {
        this.$el.focus();
      },

      // Private methods
      // ---------------

      _bindEvents: function() {
        this.$el.on("keyup." + this.id, $.proxy(this._onKeyup, this));
      },

      _onKeyup: function(e) {
        if (this._skipSearch(e)) {
          return;
        }
        this.completer.trigger(this.getTextFromHeadToCaret(), true);
      },

      // Suppress searching if it returns true.
      _skipSearch: function(clickEvent) {
        switch (clickEvent.keyCode) {
          case 9: // TAB
          case 13: // ENTER
          case 16: // SHIFT
          case 17: // CTRL
          case 18: // ALT
          case 33: // PAGEUP
          case 34: // PAGEDOWN
          case 40: // DOWN
          case 38: // UP
          case 27: // ESC
            return true;
        }
        if (clickEvent.ctrlKey)
          switch (clickEvent.keyCode) {
            case 78: // Ctrl-N
            case 80: // Ctrl-P
              return true;
          }
      }
    });

    $.fn.textcomplete.Adapter = Adapter;
  })(jQuery);

  +(function($) {
    "use strict";

    // Textarea adapter
    // ================
    //
    // Managing a textarea. It doesn't know a Dropdown.
    function Textarea(element, completer, option) {
      this.initialize(element, completer, option);
    }

    $.extend(Textarea.prototype, $.fn.textcomplete.Adapter.prototype, {
      // Public methods
      // --------------

      // Update the textarea with the given value and strategy.
      select: function(value, strategy, e) {
        var pre = this.getTextFromHeadToCaret();
        var post = this.el.value.substring(this.el.selectionEnd);
        var newSubstr = strategy.replace(value, e);
        var regExp;
        if (typeof newSubstr !== "undefined") {
          if ($.isArray(newSubstr)) {
            post = newSubstr[1] + post;
            newSubstr = newSubstr[0];
          }
          regExp = $.isFunction(strategy.match)
            ? strategy.match(pre)
            : strategy.match;
          pre = pre.replace(regExp, newSubstr);
          this.$el.val(pre + post);
          this.el.selectionStart = this.el.selectionEnd = pre.length;
        }
      },

      getTextFromHeadToCaret: function() {
        return this.el.value.substring(0, this.el.selectionEnd);
      },

      // Private methods
      // ---------------

      _getCaretRelativePosition: function() {
        var p = $.fn.textcomplete.getCaretCoordinates(
          this.el,
          this.el.selectionStart
        );
        return {
          top: p.top + this._calculateLineHeight() - this.$el.scrollTop(),
          left: p.left - this.$el.scrollLeft(),
          lineHeight: this._calculateLineHeight()
        };
      },

      _calculateLineHeight: function() {
        var lineHeight = parseInt(this.$el.css("line-height"), 10);
        if (isNaN(lineHeight)) {
          // http://stackoverflow.com/a/4515470/1297336
          var parentNode = this.el.parentNode;
          var temp = document.createElement(this.el.nodeName);
          var style = this.el.style;
          temp.setAttribute(
            "style",
            "margin:0px;padding:0px;font-family:" +
              style.fontFamily +
              ";font-size:" +
              style.fontSize
          );
          temp.innerHTML = "test";
          parentNode.appendChild(temp);
          lineHeight = temp.clientHeight;
          parentNode.removeChild(temp);
        }
        return lineHeight;
      }
    });

    $.fn.textcomplete.Textarea = Textarea;
  })(jQuery);

  +(function($) {
    "use strict";

    var sentinelChar = "吶";

    function IETextarea(element, completer, option) {
      this.initialize(element, completer, option);
      $("<span>" + sentinelChar + "</span>")
        .css({
          position: "absolute",
          top: -9999,
          left: -9999
        })
        .insertBefore(element);
    }

    $.extend(IETextarea.prototype, $.fn.textcomplete.Textarea.prototype, {
      // Public methods
      // --------------

      select: function(value, strategy, e) {
        var pre = this.getTextFromHeadToCaret();
        var post = this.el.value.substring(pre.length);
        var newSubstr = strategy.replace(value, e);
        var regExp;
        if (typeof newSubstr !== "undefined") {
          if ($.isArray(newSubstr)) {
            post = newSubstr[1] + post;
            newSubstr = newSubstr[0];
          }
          regExp = $.isFunction(strategy.match)
            ? strategy.match(pre)
            : strategy.match;
          pre = pre.replace(regExp, newSubstr);
          this.$el.val(pre + post);
          this.el.focus();
          var range = this.el.createTextRange();
          range.collapse(true);
          range.moveEnd("character", pre.length);
          range.moveStart("character", pre.length);
          range.select();
        }
      },

      getTextFromHeadToCaret: function() {
        this.el.focus();
        var range = document.selection.createRange();
        range.moveStart("character", -this.el.value.length);
        var arr = range.text.split(sentinelChar);
        return arr.length === 1 ? arr[0] : arr[1];
      }
    });

    $.fn.textcomplete.IETextarea = IETextarea;
  })(jQuery);

  // NOTE: TextComplete plugin has contenteditable support but it does not work
  //       fine especially on old IEs.
  //       Any pull requests are REALLY welcome.

  +(function($) {
    "use strict";

    // ContentEditable adapter
    // =======================
    //
    // Adapter for contenteditable elements.
    function ContentEditable(element, completer, option) {
      this.initialize(element, completer, option);
    }

    $.extend(ContentEditable.prototype, $.fn.textcomplete.Adapter.prototype, {
      // Public methods
      // --------------

      // Update the content with the given value and strategy.
      // When an dropdown item is selected, it is executed.
      select: function(value, strategy, e) {
        var pre = this.getTextFromHeadToCaret();
        // use ownerDocument instead of window to support iframes
        var sel = this.el.ownerDocument.getSelection();

        var range = sel.getRangeAt(0);
        var selection = range.cloneRange();
        selection.selectNodeContents(range.startContainer);
        var content = selection.toString();
        var post = content.substring(range.startOffset);
        var newSubstr = strategy.replace(value, e);
        var regExp;
        if (typeof newSubstr !== "undefined") {
          if ($.isArray(newSubstr)) {
            post = newSubstr[1] + post;
            newSubstr = newSubstr[0];
          }
          regExp = $.isFunction(strategy.match)
            ? strategy.match(pre)
            : strategy.match;
          pre = pre.replace(regExp, newSubstr).replace(/ $/, "&nbsp"); // &nbsp necessary at least for CKeditor to not eat spaces
          range.selectNodeContents(range.startContainer);
          range.deleteContents();

          // create temporary elements
          var preWrapper = this.el.ownerDocument.createElement("div");
          preWrapper.innerHTML = pre;
          var postWrapper = this.el.ownerDocument.createElement("div");
          postWrapper.innerHTML = post;

          // create the fragment thats inserted
          var fragment = this.el.ownerDocument.createDocumentFragment();
          var childNode;
          var lastOfPre;
          while ((childNode = preWrapper.firstChild)) {
            lastOfPre = fragment.appendChild(childNode);
          }
          while ((childNode = postWrapper.firstChild)) {
            fragment.appendChild(childNode);
          }

          // insert the fragment & jump behind the last node in "pre"
          range.insertNode(fragment);
          range.setStartAfter(lastOfPre);

          range.collapse(true);
          sel.removeAllRanges();
          sel.addRange(range);
        }
      },

      // Private methods
      // ---------------

      // Returns the caret's relative position from the contenteditable's
      // left top corner.
      //
      // Examples
      //
      //   this._getCaretRelativePosition()
      //   //=> { top: 18, left: 200, lineHeight: 16 }
      //
      // Dropdown's position will be decided using the result.
      _getCaretRelativePosition: function() {
        var range = this.el.ownerDocument
          .getSelection()
          .getRangeAt(0)
          .cloneRange();
        var wrapperNode = range.endContainer.parentNode;
        var node = this.el.ownerDocument.createElement("span");
        range.insertNode(node);
        range.selectNodeContents(node);
        range.deleteContents();
        setTimeout(function() {
          wrapperNode.normalize();
        }, 0);
        var $node = $(node);
        var position = $node.offset();
        position.left -= this.$el.offset().left;
        position.top += $node.height() - this.$el.offset().top;
        position.lineHeight = $node.height();

        // special positioning logic for iframes
        // this is typically used for contenteditables such as tinymce or ckeditor
        if (this.completer.$iframe) {
          var iframePosition = this.completer.$iframe.offset();
          position.top += iframePosition.top;
          position.left += iframePosition.left;
          // We need to get the scrollTop of the html-element inside the iframe and not of the body-element,
          // because on IE the scrollTop of the body-element (this.$el) is always zero.
          position.top -= $(
            this.completer.$iframe[0].contentWindow.document
          ).scrollTop();
        }

        $node.remove();
        return position;
      },

      // Returns the string between the first character and the caret.
      // Completer will be triggered with the result for start autocompleting.
      //
      // Example
      //
      //   // Suppose the html is '<b>hello</b> wor|ld' and | is the caret.
      //   this.getTextFromHeadToCaret()
      //   // => ' wor'  // not '<b>hello</b> wor'
      getTextFromHeadToCaret: function() {
        var range = this.el.ownerDocument.getSelection().getRangeAt(0);
        var selection = range.cloneRange();
        selection.selectNodeContents(range.startContainer);
        return selection.toString().substring(0, range.startOffset);
      }
    });

    $.fn.textcomplete.ContentEditable = ContentEditable;
  })(jQuery);

  // NOTE: TextComplete plugin has contenteditable support but it does not work
  //       fine especially on old IEs.
  //       Any pull requests are REALLY welcome.

  +(function($) {
    "use strict";

    // CKEditor adapter
    // =======================
    //
    // Adapter for CKEditor, based on contenteditable elements.
    function CKEditor(element, completer, option) {
      this.initialize(element, completer, option);
    }

    $.extend(CKEditor.prototype, $.fn.textcomplete.ContentEditable.prototype, {
      _bindEvents: function() {
        var $this = this;
        this.option.ckeditor_instance.on(
          "key",
          function(event) {
            var domEvent = event.data;
            $this._onKeyup(domEvent);
            if ($this.completer.dropdown.shown && $this._skipSearch(domEvent)) {
              return false;
            }
          },
          null,
          null,
          1
        ); // 1 = Priority = Important!
        // we actually also need the native event, as the CKEditor one is happening to late
        this.$el.on("keyup." + this.id, $.proxy(this._onKeyup, this));
      }
    });

    $.fn.textcomplete.CKEditor = CKEditor;
  })(jQuery);

  // The MIT License (MIT)
  //
  // Copyright (c) 2015 Jonathan Ong me@jongleberry.com
  //
  // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
  // associated documentation files (the "Software"), to deal in the Software without restriction,
  // including without limitation the rights to use, copy, modify, merge, publish, distribute,
  // sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
  // furnished to do so, subject to the following conditions:
  //
  // The above copyright notice and this permission notice shall be included in all copies or
  // substantial portions of the Software.
  //
  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
  // NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
  // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  //
  // https://github.com/component/textarea-caret-position

  (function($) {
    // The properties that we copy into a mirrored div.
    // Note that some browsers, such as Firefox,
    // do not concatenate properties, i.e. padding-top, bottom etc. -> padding,
    // so we have to do every single property specifically.
    var properties = [
      "direction", // RTL support
      "boxSizing",
      "width", // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
      "height",
      "overflowX",
      "overflowY", // copy the scrollbar for IE

      "borderTopWidth",
      "borderRightWidth",
      "borderBottomWidth",
      "borderLeftWidth",
      "borderStyle",

      "paddingTop",
      "paddingRight",
      "paddingBottom",
      "paddingLeft",

      // https://developer.mozilla.org/en-US/docs/Web/CSS/font
      "fontStyle",
      "fontVariant",
      "fontWeight",
      "fontStretch",
      "fontSize",
      "fontSizeAdjust",
      "lineHeight",
      "fontFamily",

      "textAlign",
      "textTransform",
      "textIndent",
      "textDecoration", // might not make a difference, but better be safe

      "letterSpacing",
      "wordSpacing",

      "tabSize",
      "MozTabSize"
    ];

    var isBrowser = typeof window !== "undefined";
    var isFirefox = isBrowser && window.mozInnerScreenX != null;

    function getCaretCoordinates(element, position, options) {
      if (!isBrowser) {
        throw new Error(
          "textarea-caret-position#getCaretCoordinates should only be called in a browser"
        );
      }

      var debug = (options && options.debug) || false;
      if (debug) {
        var el = document.querySelector(
          "#input-textarea-caret-position-mirror-div"
        );
        if (el) {
          el.parentNode.removeChild(el);
        }
      }

      // mirrored div
      var div = document.createElement("div");
      div.id = "input-textarea-caret-position-mirror-div";
      document.body.appendChild(div);

      var style = div.style;
      var computed = window.getComputedStyle
        ? getComputedStyle(element)
        : element.currentStyle; // currentStyle for IE < 9

      // default textarea styles
      style.whiteSpace = "pre-wrap";
      if (element.nodeName !== "INPUT") style.wordWrap = "break-word"; // only for textarea-s

      // position off-screen
      style.position = "absolute"; // required to return coordinates properly
      if (!debug) style.visibility = "hidden"; // not 'display: none' because we want rendering

      // transfer the element's properties to the div
      properties.forEach(function(prop) {
        style[prop] = computed[prop];
      });

      if (isFirefox) {
        // Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
        if (element.scrollHeight > parseInt(computed.height))
          style.overflowY = "scroll";
      } else {
        style.overflow = "hidden"; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
      }

      div.textContent = element.value.substring(0, position);
      // the second special handling for input type="text" vs textarea: spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037
      if (element.nodeName === "INPUT")
        div.textContent = div.textContent.replace(/\s/g, "\u00a0");

      var span = document.createElement("span");
      // Wrapping must be replicated *exactly*, including when a long word gets
      // onto the next line, with whitespace at the end of the line before (#7).
      // The  *only* reliable way to do that is to copy the *entire* rest of the
      // textarea's content into the <span> created at the caret position.
      // for inputs, just '.' would be enough, but why bother?
      span.textContent = element.value.substring(position) || "."; // || because a completely empty faux span doesn't render at all
      div.appendChild(span);

      var coordinates = {
        top: span.offsetTop + parseInt(computed["borderTopWidth"]),
        left: span.offsetLeft + parseInt(computed["borderLeftWidth"])
      };

      if (debug) {
        span.style.backgroundColor = "#aaa";
      } else {
        document.body.removeChild(div);
      }

      return coordinates;
    }

    $.fn.textcomplete.getCaretCoordinates = getCaretCoordinates;
  })(jQuery);

  return jQuery;
});
{
  "data": [
    { "name": "Afghanistan", "code": "AF" },
    { "name": "Åland Islands", "code": "AX" },
    { "name": "Albania", "code": "AL" },
    { "name": "Algeria", "code": "DZ" },
    { "name": "American Samoa", "code": "AS" },
    { "name": "AndorrA", "code": "AD" },
    { "name": "Angola", "code": "AO" },
    { "name": "Anguilla", "code": "AI" },
    { "name": "Antarctica", "code": "AQ" },
    { "name": "Antigua and Barbuda", "code": "AG" },
    { "name": "Argentina", "code": "AR" },
    { "name": "Armenia", "code": "AM" },
    { "name": "Aruba", "code": "AW" },
    { "name": "Australia", "code": "AU" },
    { "name": "Austria", "code": "AT" },
    { "name": "Azerbaijan", "code": "AZ" },
    { "name": "Bahamas", "code": "BS" },
    { "name": "Bahrain", "code": "BH" },
    { "name": "Bangladesh", "code": "BD" },
    { "name": "Barbados", "code": "BB" },
    { "name": "Belarus", "code": "BY" },
    { "name": "Belgium", "code": "BE" },
    { "name": "Belize", "code": "BZ" },
    { "name": "Benin", "code": "BJ" },
    { "name": "Bermuda", "code": "BM" },
    { "name": "Bhutan", "code": "BT" },
    { "name": "Bolivia", "code": "BO" },
    { "name": "Bosnia and Herzegovina", "code": "BA" },
    { "name": "Botswana", "code": "BW" },
    { "name": "Bouvet Island", "code": "BV" },
    { "name": "Brazil", "code": "BR" },
    { "name": "British Indian Ocean Territory", "code": "IO" },
    { "name": "Brunei Darussalam", "code": "BN" },
    { "name": "Bulgaria", "code": "BG" },
    { "name": "Burkina Faso", "code": "BF" },
    { "name": "Burundi", "code": "BI" },
    { "name": "Cambodia", "code": "KH" },
    { "name": "Cameroon", "code": "CM" },
    { "name": "Canada", "code": "CA" },
    { "name": "Cape Verde", "code": "CV" },
    { "name": "Cayman Islands", "code": "KY" },
    { "name": "Central African Republic", "code": "CF" },
    { "name": "Chad", "code": "TD" },
    { "name": "Chile", "code": "CL" },
    { "name": "China", "code": "CN" },
    { "name": "Christmas Island", "code": "CX" },
    { "name": "Cocos (Keeling) Islands", "code": "CC" },
    { "name": "Colombia", "code": "CO" },
    { "name": "Comoros", "code": "KM" },
    { "name": "Congo", "code": "CG" },
    { "name": "Congo, The Democratic Republic of the", "code": "CD" },
    { "name": "Cook Islands", "code": "CK" },
    { "name": "Costa Rica", "code": "CR" },
    { "name": "Cote D'Ivoire", "code": "CI" },
    { "name": "Croatia", "code": "HR" },
    { "name": "Cuba", "code": "CU" },
    { "name": "Cyprus", "code": "CY" },
    { "name": "Czech Republic", "code": "CZ" },
    { "name": "Denmark", "code": "DK" },
    { "name": "Djibouti", "code": "DJ" },
    { "name": "Dominica", "code": "DM" },
    { "name": "Dominican Republic", "code": "DO" },
    { "name": "Ecuador", "code": "EC" },
    { "name": "Egypt", "code": "EG" },
    { "name": "El Salvador", "code": "SV" },
    { "name": "Equatorial Guinea", "code": "GQ" },
    { "name": "Eritrea", "code": "ER" },
    { "name": "Estonia", "code": "EE" },
    { "name": "Ethiopia", "code": "ET" },
    { "name": "Falkland Islands (Malvinas)", "code": "FK" },
    { "name": "Faroe Islands", "code": "FO" },
    { "name": "Fiji", "code": "FJ" },
    { "name": "Finland", "code": "FI" },
    { "name": "France", "code": "FR" },
    { "name": "French Guiana", "code": "GF" },
    { "name": "French Polynesia", "code": "PF" },
    { "name": "French Southern Territories", "code": "TF" },
    { "name": "Gabon", "code": "GA" },
    { "name": "Gambia", "code": "GM" },
    { "name": "Georgia", "code": "GE" },
    { "name": "Germany", "code": "DE" },
    { "name": "Ghana", "code": "GH" },
    { "name": "Gibraltar", "code": "GI" },
    { "name": "Greece", "code": "GR" },
    { "name": "Greenland", "code": "GL" },
    { "name": "Grenada", "code": "GD" },
    { "name": "Guadeloupe", "code": "GP" },
    { "name": "Guam", "code": "GU" },
    { "name": "Guatemala", "code": "GT" },
    { "name": "Guernsey", "code": "GG" },
    { "name": "Guinea", "code": "GN" },
    { "name": "Guinea-Bissau", "code": "GW" },
    { "name": "Guyana", "code": "GY" },
    { "name": "Haiti", "code": "HT" },
    { "name": "Heard Island and Mcdonald Islands", "code": "HM" },
    { "name": "Holy See (Vatican City State)", "code": "VA" },
    { "name": "Honduras", "code": "HN" },
    { "name": "Hong Kong", "code": "HK" },
    { "name": "Hungary", "code": "HU" },
    { "name": "Iceland", "code": "IS" },
    { "name": "India", "code": "IN" },
    { "name": "Indonesia", "code": "ID" },
    { "name": "Iran, Islamic Republic Of", "code": "IR" },
    { "name": "Iraq", "code": "IQ" },
    { "name": "Ireland", "code": "IE" },
    { "name": "Isle of Man", "code": "IM" },
    { "name": "Israel", "code": "IL" },
    { "name": "Italy", "code": "IT" },
    { "name": "Jamaica", "code": "JM" },
    { "name": "Japan", "code": "JP" },
    { "name": "Jersey", "code": "JE" },
    { "name": "Jordan", "code": "JO" },
    { "name": "Kazakhstan", "code": "KZ" },
    { "name": "Kenya", "code": "KE" },
    { "name": "Kiribati", "code": "KI" },
    { "name": "Korea, Democratic People'S Republic of", "code": "KP" },
    { "name": "Korea, Republic of", "code": "KR" },
    { "name": "Kuwait", "code": "KW" },
    { "name": "Kyrgyzstan", "code": "KG" },
    { "name": "Lao People'S Democratic Republic", "code": "LA" },
    { "name": "Latvia", "code": "LV" },
    { "name": "Lebanon", "code": "LB" },
    { "name": "Lesotho", "code": "LS" },
    { "name": "Liberia", "code": "LR" },
    { "name": "Libyan Arab Jamahiriya", "code": "LY" },
    { "name": "Liechtenstein", "code": "LI" },
    { "name": "Lithuania", "code": "LT" },
    { "name": "Luxembourg", "code": "LU" },
    { "name": "Macao", "code": "MO" },
    { "name": "Macedonia, The Former Yugoslav Republic of", "code": "MK" },
    { "name": "Madagascar", "code": "MG" },
    { "name": "Malawi", "code": "MW" },
    { "name": "Malaysia", "code": "MY" },
    { "name": "Maldives", "code": "MV" },
    { "name": "Mali", "code": "ML" },
    { "name": "Malta", "code": "MT" },
    { "name": "Marshall Islands", "code": "MH" },
    { "name": "Martinique", "code": "MQ" },
    { "name": "Mauritania", "code": "MR" },
    { "name": "Mauritius", "code": "MU" },
    { "name": "Mayotte", "code": "YT" },
    { "name": "Mexico", "code": "MX" },
    { "name": "Micronesia, Federated States of", "code": "FM" },
    { "name": "Moldova, Republic of", "code": "MD" },
    { "name": "Monaco", "code": "MC" },
    { "name": "Mongolia", "code": "MN" },
    { "name": "Montserrat", "code": "MS" },
    { "name": "Morocco", "code": "MA" },
    { "name": "Mozambique", "code": "MZ" },
    { "name": "Myanmar", "code": "MM" },
    { "name": "Namibia", "code": "NA" },
    { "name": "Nauru", "code": "NR" },
    { "name": "Nepal", "code": "NP" },
    { "name": "Netherlands", "code": "NL" },
    { "name": "Netherlands Antilles", "code": "AN" },
    { "name": "New Caledonia", "code": "NC" },
    { "name": "New Zealand", "code": "NZ" },
    { "name": "Nicaragua", "code": "NI" },
    { "name": "Niger", "code": "NE" },
    { "name": "Nigeria", "code": "NG" },
    { "name": "Niue", "code": "NU" },
    { "name": "Norfolk Island", "code": "NF" },
    { "name": "Northern Mariana Islands", "code": "MP" },
    { "name": "Norway", "code": "NO" },
    { "name": "Oman", "code": "OM" },
    { "name": "Pakistan", "code": "PK" },
    { "name": "Palau", "code": "PW" },
    { "name": "Palestinian Territory, Occupied", "code": "PS" },
    { "name": "Panama", "code": "PA" },
    { "name": "Papua New Guinea", "code": "PG" },
    { "name": "Paraguay", "code": "PY" },
    { "name": "Peru", "code": "PE" },
    { "name": "Philippines", "code": "PH" },
    { "name": "Pitcairn", "code": "PN" },
    { "name": "Poland", "code": "PL" },
    { "name": "Portugal", "code": "PT" },
    { "name": "Puerto Rico", "code": "PR" },
    { "name": "Qatar", "code": "QA" },
    { "name": "Reunion", "code": "RE" },
    { "name": "Romania", "code": "RO" },
    { "name": "Russian Federation", "code": "RU" },
    { "name": "RWANDA", "code": "RW" },
    { "name": "Saint Helena", "code": "SH" },
    { "name": "Saint Kitts and Nevis", "code": "KN" },
    { "name": "Saint Lucia", "code": "LC" },
    { "name": "Saint Pierre and Miquelon", "code": "PM" },
    { "name": "Saint Vincent and the Grenadines", "code": "VC" },
    { "name": "Samoa", "code": "WS" },
    { "name": "San Marino", "code": "SM" },
    { "name": "Sao Tome and Principe", "code": "ST" },
    { "name": "Saudi Arabia", "code": "SA" },
    { "name": "Senegal", "code": "SN" },
    { "name": "Serbia and Montenegro", "code": "CS" },
    { "name": "Seychelles", "code": "SC" },
    { "name": "Sierra Leone", "code": "SL" },
    { "name": "Singapore", "code": "SG" },
    { "name": "Slovakia", "code": "SK" },
    { "name": "Slovenia", "code": "SI" },
    { "name": "Solomon Islands", "code": "SB" },
    { "name": "Somalia", "code": "SO" },
    { "name": "South Africa", "code": "ZA" },
    { "name": "South Georgia and the South Sandwich Islands", "code": "GS" },
    { "name": "Spain", "code": "ES" },
    { "name": "Sri Lanka", "code": "LK" },
    { "name": "Sudan", "code": "SD" },
    { "name": "Suriname", "code": "SR" },
    { "name": "Svalbard and Jan Mayen", "code": "SJ" },
    { "name": "Swaziland", "code": "SZ" },
    { "name": "Sweden", "code": "SE" },
    { "name": "Switzerland", "code": "CH" },
    { "name": "Syrian Arab Republic", "code": "SY" },
    { "name": "Taiwan, Province of China", "code": "TW" },
    { "name": "Tajikistan", "code": "TJ" },
    { "name": "Tanzania, United Republic of", "code": "TZ" },
    { "name": "Thailand", "code": "TH" },
    { "name": "Timor-Leste", "code": "TL" },
    { "name": "Togo", "code": "TG" },
    { "name": "Tokelau", "code": "TK" },
    { "name": "Tonga", "code": "TO" },
    { "name": "Trinidad and Tobago", "code": "TT" },
    { "name": "Tunisia", "code": "TN" },
    { "name": "Turkey", "code": "TR" },
    { "name": "Turkmenistan", "code": "TM" },
    { "name": "Turks and Caicos Islands", "code": "TC" },
    { "name": "Tuvalu", "code": "TV" },
    { "name": "Uganda", "code": "UG" },
    { "name": "Ukraine", "code": "UA" },
    { "name": "United Arab Emirates", "code": "AE" },
    { "name": "United Kingdom", "code": "GB" },
    { "name": "United States", "code": "US" },
    { "name": "United States Minor Outlying Islands", "code": "UM" },
    { "name": "Uruguay", "code": "UY" },
    { "name": "Uzbekistan", "code": "UZ" },
    { "name": "Vanuatu", "code": "VU" },
    { "name": "Venezuela", "code": "VE" },
    { "name": "Viet Nam", "code": "VN" },
    { "name": "Virgin Islands, British", "code": "VG" },
    { "name": "Virgin Islands, U.S.", "code": "VI" },
    { "name": "Wallis and Futuna", "code": "WF" },
    { "name": "Western Sahara", "code": "EH" },
    { "name": "Yemen", "code": "YE" },
    { "name": "Zambia", "code": "ZM" },
    { "name": "Zimbabwe", "code": "ZW" }
  ]
}
.textcomplete-dropdown {
  border: 1px solid #ddd;
  background-color: white;
}

.textcomplete-dropdown li {
  border-top: 1px solid #ddd;
  padding: 2px 5px;
}

.textcomplete-dropdown li:first-child {
  border-top: none;
}

.textcomplete-dropdown li:hover,
.textcomplete-dropdown .active {
  background-color: rgb(110, 183, 219);
}

.textcomplete-dropdown {
  list-style: none;
  padding: 0;
  margin: 0;
}

.textcomplete-dropdown a:hover {
  cursor: pointer;
}
<mvc:View controllerName="com.demo.textarea.autocomplete.controller.App" 
    xmlns="sap.m" 
    xmlns:l="sap.ui.layout" 
    xmlns:core="sap.ui.core" 
    xmlns:mvc="sap.ui.core.mvc">
    <Page title="Demo - TextArea Autocomplete">
        <content>
            <FlexBox width="100%" direction="Column" backgroundDesign="Solid" displayInline="true">
                <HBox width="100%">
                    <TextArea id="textArea_01" height="30%" cols="100"/>
                </HBox>
            </FlexBox>
        </content>
    </Page>
</mvc:View>