123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553 |
- /*!
- * jQuery.textcomplete.js
- *
- * Repositiory: https://github.com/yuku-t/jquery-textcomplete
- * License: MIT
- * Author: Yuku Takahashi
- */
- ;(function ($) {
- 'use strict';
- /**
- * Exclusive execution control utility.
- */
- var lock = function (func) {
- var free, locked;
- free = function () { locked = false; };
- return function () {
- var args;
- if (locked) return;
- locked = true;
- args = toArray(arguments);
- args.unshift(free);
- func.apply(this, args);
- };
- };
- /**
- * Convert arguments into a real array.
- */
- var toArray = function (args) {
- var result;
- result = Array.prototype.slice.call(args);
- return result;
- };
- /**
- * Get the styles of any element from property names.
- */
- var getStyles = (function () {
- var color;
- color = $('<div></div>').css(['color']).color;
- if (typeof color !== 'undefined') {
- return function ($el, properties) {
- return $el.css(properties);
- };
- } else { // for jQuery 1.8 or below
- return function ($el, properties) {
- var styles;
- styles = {};
- $.each(properties, function (i, property) {
- styles[property] = $el.css(property);
- });
- return styles;
- };
- }
- })();
- /**
- * Default template function.
- */
- var identity = function (obj) { return obj; };
- /**
- * 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);
- });
- }
- };
- };
- /**
- * Determine if the array contains a given value.
- */
- var include = function (array, value) {
- var i, l;
- if (array.indexOf) return array.indexOf(value) != -1;
- for (i = 0, l = array.length; i < l; i++) {
- if (array[i] === value) return true;
- }
- return false;
- };
- /**
- * Textarea manager class.
- */
- var Completer = (function () {
- var html, css, $baseWrapper, $baseList, _id;
- html = {
- wrapper: '<div class="textcomplete-wrapper"></div>',
- list: '<ul class="dropdown-menu"></ul>'
- };
- css = {
- wrapper: {
- position: 'relative'
- },
- list: {
- position: 'absolute',
- top: 0,
- left: 0,
- zIndex: '100',
- display: 'none'
- }
- };
- $baseWrapper = $(html.wrapper).css(css.wrapper);
- $baseList = $(html.list).css(css.list);
- _id = 0;
- function Completer($el) {
- var focus;
- this.el = $el.get(0); // textarea element
- focus = this.el === document.activeElement;
- // Cannot wrap $el at initialize method lazily due to Firefox's behavior.
- this.$el = wrapElement($el); // Focus is lost
- this.id = 'textComplete' + _id++;
- this.strategies = [];
- if (focus) {
- this.initialize();
- this.$el.focus();
- } else {
- this.$el.one('focus.textComplete', $.proxy(this.initialize, this));
- }
- }
- /**
- * Completer's public methods
- */
- $.extend(Completer.prototype, {
- /**
- * Prepare ListView and bind events.
- */
- initialize: function () {
- var $list, globalEvents;
- $list = $baseList.clone();
- this.listView = new ListView($list, this);
- this.$el
- .before($list)
- .on({
- 'keyup.textComplete': $.proxy(this.onKeyup, this),
- 'keydown.textComplete': $.proxy(this.listView.onKeydown,
- this.listView)
- });
- globalEvents = {};
- globalEvents['click.' + this.id] = $.proxy(this.onClickDocument, this);
- globalEvents['keyup.' + this.id] = $.proxy(this.onKeyupDocument, this);
- $(document).on(globalEvents);
- },
- /**
- * Register strategies to the completer.
- */
- register: function (strategies) {
- this.strategies = this.strategies.concat(strategies);
- },
- /**
- * Show autocomplete list next to the caret.
- */
- renderList: function (data) {
- if (this.clearAtNext) {
- this.listView.clear();
- this.clearAtNext = false;
- }
- if (data.length) {
- if (!this.listView.shown) {
- this.listView
- .setPosition(this.getCaretPosition())
- .clear()
- .activate();
- this.listView.strategy = this.strategy;
- }
- data = data.slice(0, this.strategy.maxCount);
- this.listView.render(data);
- }
- if (!this.listView.data.length && this.listView.shown) {
- this.listView.deactivate();
- }
- },
- searchCallbackFactory: function (free) {
- var self = this;
- return function (data, keep) {
- self.renderList(data);
- if (!keep) {
- // This is the last callback for this search.
- free();
- self.clearAtNext = true;
- }
- };
- },
- /**
- * Keyup event handler.
- */
- onKeyup: function (e) {
- var searchQuery, term;
- if (this.skipSearch(e)) { return; }
- searchQuery = this.extractSearchQuery(this.getTextFromHeadToCaret());
- if (searchQuery.length) {
- term = searchQuery[1];
- if (this.term === term) return; // Ignore shift-key or something.
- this.term = term;
- this.search(searchQuery);
- } else {
- this.term = null;
- this.listView.deactivate();
- }
- },
- /**
- * Suppress searching if it returns true.
- */
- skipSearch: function (e) {
- if (this.skipNextKeyup) {
- this.skipNextKeyup = false;
- return true;
- }
- switch (e.keyCode) {
- case 40:
- case 38:
- return true;
- }
- },
- onSelect: function (value) {
- var pre, post, newSubStr;
- pre = this.getTextFromHeadToCaret();
- post = this.el.value.substring(this.el.selectionEnd);
- newSubStr = this.strategy.replace(value);
- if ($.isArray(newSubStr)) {
- post = newSubStr[1] + post;
- newSubStr = newSubStr[0];
- }
- pre = pre.replace(this.strategy.match, newSubStr);
- this.$el.val(pre + post)
- .trigger('change')
- .trigger('textComplete:select', value);
- this.el.focus();
- this.el.selectionStart = this.el.selectionEnd = pre.length;
- this.skipNextKeyup = true;
- },
- /**
- * Global click event handler.
- */
- onClickDocument: function (e) {
- if (e.originalEvent && !e.originalEvent.keepTextCompleteDropdown) {
- this.listView.deactivate();
- }
- },
- /**
- * Global keyup event handler.
- */
- onKeyupDocument: function (e) {
- if (this.listView.shown && e.keyCode === 27) { // ESC
- this.listView.deactivate();
- this.$el.focus();
- }
- },
- /**
- * Remove all event handlers and the wrapper element.
- */
- destroy: function () {
- var $wrapper;
- this.$el.off('.textComplete');
- $(document).off('.' + this.id);
- if (this.listView) { this.listView.destroy(); }
- $wrapper = this.$el.parent();
- $wrapper.after(this.$el).remove();
- this.$el.data('textComplete', void 0);
- this.$el = null;
- },
- // Helper methods
- // ==============
- /**
- * Returns caret's relative coordinates from textarea's left top corner.
- */
- getCaretPosition: function () {
- // Browser native API does not provide the way to know the position of
- // caret in pixels, so that here we use a kind of hack to accomplish
- // the aim. First of all it puts a div element and completely copies
- // the textarea's style to the element, then it inserts the text and a
- // span element into the textarea.
- // Consequently, the span element's position is the thing what we want.
- if (this.el.selectionEnd === 0) return;
- var properties, css, $div, $span, position, dir;
- dir = this.$el.attr('dir') || this.$el.css('direction');
- properties = ['border-width', 'font-family', 'font-size', 'font-style',
- 'font-variant', 'font-weight', 'height', 'letter-spacing',
- 'word-spacing', 'line-height', 'text-decoration', 'text-align',
- 'width', 'padding-top', 'padding-right', 'padding-bottom',
- 'padding-left', 'margin-top', 'margin-right', 'margin-bottom',
- 'margin-left'
- ];
- css = $.extend({
- position: 'absolute',
- overflow: 'auto',
- 'white-space': 'pre-wrap',
- top: 0,
- left: -9999,
- direction: dir
- }, getStyles(this.$el, properties));
- $div = $('<div></div>').css(css).text(this.getTextFromHeadToCaret());
- $span = $('<span></span>').text('.').appendTo($div);
- this.$el.before($div);
- position = $span.position();
- position.top += $span.height() - this.$el.scrollTop();
- if (dir === 'rtl') { position.left -= this.listView.$el.width(); }
- $div.remove();
- return position;
- },
- getTextFromHeadToCaret: function () {
- var text, selectionEnd, range;
- selectionEnd = this.el.selectionEnd;
- if (typeof selectionEnd === 'number') {
- text = this.el.value.substring(0, selectionEnd);
- } else if (document.selection) {
- range = this.el.createTextRange();
- range.moveStart('character', 0);
- range.moveEnd('textedit');
- text = range.text;
- }
- return text;
- },
- /**
- * Parse the value of textarea and extract search query.
- */
- extractSearchQuery: function (text) {
- // If a search query found, it returns used strategy and the query
- // term. If the caret is currently in a code block or search query does
- // not found, it returns an empty array.
- var i, l, strategy, match;
- for (i = 0, l = this.strategies.length; i < l; i++) {
- strategy = this.strategies[i];
- match = text.match(strategy.match);
- if (match) { return [strategy, match[strategy.index]]; }
- }
- return [];
- },
- search: lock(function (free, searchQuery) {
- var term;
- this.strategy = searchQuery[0];
- term = searchQuery[1];
- this.strategy.search(term, this.searchCallbackFactory(free));
- })
- });
- /**
- * Completer's private functions
- */
- var wrapElement = function ($el) {
- return $el.wrap($baseWrapper.clone().css('display', $el.css('display')));
- };
- return Completer;
- })();
- /**
- * Dropdown menu manager class.
- */
- var ListView = (function () {
- function ListView($el, completer) {
- this.data = [];
- this.$el = $el;
- this.index = 0;
- this.completer = completer;
- this.$el.on('click.textComplete', 'li.textcomplete-item',
- $.proxy(this.onClick, this));
- }
- $.extend(ListView.prototype, {
- shown: false,
- render: function (data) {
- var html, i, l, index, val;
- html = '';
- for (i = 0, l = data.length; i < l; i++) {
- val = data[i];
- if (include(this.data, val)) continue;
- index = this.data.length;
- this.data.push(val);
- html += '<li class="textcomplete-item" data-index="' + index + '"><a>';
- html += this.strategy.template(val);
- html += '</a></li>';
- if (this.data.length === this.strategy.maxCount) break;
- }
- this.$el.append(html);
- if (!this.data.length) {
- this.deactivate();
- } else {
- this.activateIndexedItem();
- }
- },
- clear: function () {
- this.data = [];
- this.$el.html('');
- this.index = 0;
- return this;
- },
- activateIndexedItem: function () {
- this.$el.find('.active').removeClass('active');
- this.getActiveItem().addClass('active');
- },
- getActiveItem: function () {
- return $(this.$el.children().get(this.index));
- },
- activate: function () {
- if (!this.shown) {
- this.$el.show();
- this.completer.$el.trigger('textComplete:show');
- this.shown = true;
- }
- return this;
- },
- deactivate: function () {
- if (this.shown) {
- this.$el.hide();
- this.completer.$el.trigger('textComplete:hide');
- this.shown = false;
- this.data = this.index = null;
- }
- return this;
- },
- setPosition: function (position) {
- this.$el.css(position);
- return this;
- },
- select: function (index) {
- var self = this;
- this.completer.onSelect(this.data[index]);
- // Deactive at next tick to allow other event handlers to know whether
- // the dropdown has been shown or not.
- setTimeout(function () { self.deactivate(); }, 0);
- },
- onKeydown: function (e) {
- if (!this.shown) return;
- if (e.keyCode === 38) { // UP
- e.preventDefault();
- if (this.index === 0) {
- this.index = this.data.length-1;
- } else {
- this.index -= 1;
- }
- this.activateIndexedItem();
- } else if (e.keyCode === 40) { // DOWN
- e.preventDefault();
- if (this.index === this.data.length - 1) {
- this.index = 0;
- } else {
- this.index += 1;
- }
- this.activateIndexedItem();
- } else if (e.keyCode === 13 || e.keyCode === 9) { // ENTER or TAB
- e.preventDefault();
- this.select(parseInt(this.getActiveItem().data('index'), 10));
- }
- },
- onClick: function (e) {
- var $e = $(e.target);
- e.originalEvent.keepTextCompleteDropdown = true;
- if (!$e.hasClass('textcomplete-item')) {
- $e = $e.parents('li.textcomplete-item');
- }
- this.select(parseInt($e.data('index'), 10));
- },
- destroy: function () {
- this.deactivate();
- this.$el.off('click.textComplete').remove();
- this.$el = null;
- }
- });
- return ListView;
- })();
- $.fn.textcomplete = function (strategies) {
- var i, l, strategy, dataKey;
- dataKey = 'textComplete';
- if (strategies === 'destroy') {
- return this.each(function () {
- var completer = $(this).data(dataKey);
- if (completer) { completer.destroy(); }
- });
- }
- for (i = 0, l = strategies.length; i < l; i++) {
- strategy = strategies[i];
- if (!strategy.template) {
- strategy.template = identity;
- }
- if (strategy.index == null) {
- strategy.index = 2;
- }
- if (strategy.cache) {
- strategy.search = memoize(strategy.search);
- }
- strategy.maxCount || (strategy.maxCount = 10);
- }
- return this.each(function () {
- var $this, completer;
- $this = $(this);
- completer = $this.data(dataKey);
- if (!completer) {
- completer = new Completer($this);
- $this.data(dataKey, completer);
- }
- completer.register(strategies);
- });
- };
- })(window.jQuery || window.Zepto);
|