Skip to content
Extraits de code Groupes Projets
jquery.mentionsInput.js 13,9 ko
Newer Older
  • Learn to ignore specific revisions
  • Paul Hill's avatar
    Paul Hill a validé
    // @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt Expat
    
    /*
     * Mentions Input
    
     * Written by: Kenneth Auchenberg (Podio)
     *
     * Using underscore.js
     *
     * License: MIT License - http://www.opensource.org/licenses/mit-license.php
    
     * Prevent replacing the wrong text by marking the replacement position with a special character
     * Don't add a space after inserting a mention
     * Only use the first div as a wrapperBox
    
     * Binded paste event on input box to trigger contacts search for autocompletion while adding mention via clipboard
    
     */
    
    (function ($, _, undefined) {
    
      // Settings
    
      var KEY = { PASTE : 118, BACKSPACE : 8, TAB : 9, RETURN : 13, ESC : 27, LEFT : 37, UP : 38, RIGHT : 39,
                  DOWN : 40, COMMA : 188, SPACE : 32, HOME : 36, END : 35 }; // Keys "enum"
    
      var defaultSettings = {
        triggerChar   : '@',
        onDataRequest : $.noop,
        minChars      : 2,
        showAvatars   : true,
        elastic       : true,
        classes       : {
          autoCompleteItemActive : "active"
        },
        templates     : {
          wrapper                    : _.template('<div class="mentions-input-box"></div>'),
          autocompleteList           : _.template('<div class="mentions-autocomplete-list"></div>'),
          autocompleteListItem       : _.template('<li data-ref-id="<%= id %>" data-ref-type="<%= type %>" data-display="<%= display %>"><%= content %></li>'),
          autocompleteListItemAvatar : _.template('<img  src="<%= avatar %>" />'),
          autocompleteListItemIcon   : _.template('<div class="icon <%= icon %>"></div>'),
    
          mentionsOverlay            : _.template('<div class="mentions-box"><div class="mentions"><div></div></div></div>'),
    
          mentionItemSyntax          : _.template('@[<%= value %>](<%= type %>:<%= id %>)'),
          mentionItemHighlight       : _.template('<strong><span><%= value %></span></strong>')
    
        }
      };
    
      var utils = {
        htmlEncode       : function (str) {
          return _.escape(str);
        },
        highlightTerm    : function (value, term) {
          if (!term && !term.length) {
            return value;
          }
          return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>");
        },
        setCaratPosition : function (domNode, caretPos) {
          if (domNode.createTextRange) {
            var range = domNode.createTextRange();
            range.move('character', caretPos);
            range.select();
          } else {
            if (domNode.selectionStart) {
              domNode.focus();
              domNode.setSelectionRange(caretPos, caretPos);
            } else {
              domNode.focus();
            }
          }
    
        },
        rtrim: function(string) {
          return string.replace(/\s+$/,"");
    
      var MentionsInput = function (settings) {
    
        var domInput, elmInputBox, elmInputWrapper, elmAutocompleteList, elmWrapperBox, elmMentionsOverlay, elmActiveAutoCompleteItem;
    
        var mentionsCollection = [];
    
        var autocompleteItemCollection = {};
    
        var inputBuffer = [];
    
    Dan Hansen's avatar
    Dan Hansen a validé
        var currentDataQuery = '';
    
        var mentionChar = "\u200B"; // zero width space
    
        settings = $.extend(true, {}, defaultSettings, settings );
    
    
        function initTextarea() {
    
          elmInputBox = $(domInput);
    
    
          if (elmInputBox.attr('data-mentions-input') == 'true') {
            return;
          }
    
          elmInputWrapper = elmInputBox.parent();
          elmWrapperBox = $(settings.templates.wrapper());
          elmInputBox.wrapAll(elmWrapperBox);
    
          elmWrapperBox = elmInputWrapper.find('> div').first();
    
    
          elmInputBox.attr('data-mentions-input', 'true');
          elmInputBox.bind('keydown', onInputBoxKeyDown);
          elmInputBox.bind('keypress', onInputBoxKeyPress);
    
          elmInputBox.bind('paste',onInputBoxPaste);
    
          elmInputBox.bind('input', onInputBoxInput);
          elmInputBox.bind('click', onInputBoxClick);
    
          elmInputBox.bind('blur', onInputBoxBlur);
    
          // Elastic textareas, internal setting for the Dispora guys
          if( settings.elastic ) {
    
            elmInputBox.elastic();
          }
    
        }
    
        function initAutocomplete() {
          elmAutocompleteList = $(settings.templates.autocompleteList());
          elmAutocompleteList.appendTo(elmWrapperBox);
    
          elmAutocompleteList.delegate('li', 'mousedown', onAutoCompleteItemClick);
    
        }
    
        function initMentionsOverlay() {
          elmMentionsOverlay = $(settings.templates.mentionsOverlay());
          elmMentionsOverlay.prependTo(elmWrapperBox);
        }
    
    
        function updateValues() {
    
          var syntaxMessage = getInputBoxValue();
    
          _.each(mentionsCollection, function (mention) {
    
            var textSyntax = settings.templates.mentionItemSyntax(mention);
    
            syntaxMessage = syntaxMessage.replace(mentionChar + mention.value, textSyntax);
    
          });
    
          var mentionText = utils.htmlEncode(syntaxMessage);
    
          _.each(mentionsCollection, function (mention) {
    
            var formattedMention = _.extend({}, mention, {value: mentionChar + utils.htmlEncode(mention.value)});
    
            var textSyntax = settings.templates.mentionItemSyntax(formattedMention);
            var textHighlight = settings.templates.mentionItemHighlight(formattedMention);
    
    
            mentionText = mentionText.replace(textSyntax, textHighlight);
          });
    
          mentionText = mentionText.replace(/\n/g, '<br />');
          mentionText = mentionText.replace(/ {2}/g, '&nbsp; ');
    
          elmInputBox.data('messageText', syntaxMessage);
    
          elmMentionsOverlay.find('div > div').html(mentionText);
    
        }
    
        function resetBuffer() {
          inputBuffer = [];
        }
    
        function updateMentionsCollection() {
          var inputText = getInputBoxValue();
    
          mentionsCollection = _.reject(mentionsCollection, function (mention, index) {
    
            return !mention.value || inputText.indexOf(mention.value) == -1;
    
          });
          mentionsCollection = _.compact(mentionsCollection);
        }
    
    
    danielgrippi's avatar
    danielgrippi a validé
        function addMention(mention) {
    
          var currentMessage = getInputBoxValue();
    
          // Using a regex to figure out positions
          var regex = new RegExp("\\" + settings.triggerChar + currentDataQuery, "gi");
          regex.exec(currentMessage);
    
          var startCaretPosition = regex.lastIndex - currentDataQuery.length - 1;
          var currentCaretPosition = regex.lastIndex;
    
          var start = currentMessage.substr(0, startCaretPosition);
          var end = currentMessage.substr(currentCaretPosition, currentMessage.length);
    
          var startEndIndex = (start + mention.value).length + 1;
    
    danielgrippi's avatar
    danielgrippi a validé
          mentionsCollection.push(mention);
    
    
          // Cleaning before inserting the value, otherwise auto-complete would be triggered with "old" inputbuffer
          resetBuffer();
          currentDataQuery = '';
          hideAutoComplete();
    
          // Mentions & syntax message
    
          var updatedMessageText = start + mentionChar + mention.value + end;
    
          elmInputBox.val(updatedMessageText);
    
    
          // Set correct focus and selection
          elmInputBox.focus();
          utils.setCaratPosition(elmInputBox[0], startEndIndex);
        }
    
        function getInputBoxValue() {
          return $.trim(elmInputBox.val());
        }
    
        function onAutoCompleteItemClick(e) {
    
          var elmTarget = $(this);
          var mention = autocompleteItemCollection[elmTarget.attr('data-uid')];
    
          addMention(mention);
    
    
          return false;
        }
    
        function onInputBoxClick(e) {
          resetBuffer();
        }
    
    
        function onInputBoxBlur(e) {
          hideAutoComplete();
        }
    
    
        function onInputBoxPaste(e) {
          pastedData = e.originalEvent.clipboardData.getData("text/plain");
          dataArray = pastedData.split("");
          _.each(dataArray, function(value) {
            inputBuffer.push(value);
          });
        }
    
        function onInputBoxInput(e) {
    
          updateMentionsCollection();
          hideAutoComplete();
    
          var triggerCharIndex = _.lastIndexOf(inputBuffer, settings.triggerChar);
          if (triggerCharIndex > -1) {
            currentDataQuery = inputBuffer.slice(triggerCharIndex + 1).join('');
    
            currentDataQuery = utils.rtrim(currentDataQuery);
    
    
            _.defer(_.bind(doSearch, this, currentDataQuery));
          }
        }
    
        function onInputBoxKeyPress(e) {
    
          // Excluding ctrl+v from key press event in firefox
          if (!((e.which === KEY.PASTE && e.ctrlKey) || (e.keyCode === KEY.BACKSPACE))) {
    
    Florian Staudacher's avatar
    Florian Staudacher a validé
            var typedValue = String.fromCharCode(e.which || e.keyCode);
            inputBuffer.push(typedValue);
          }
    
        }
    
        function onInputBoxKeyDown(e) {
    
          // This also matches HOME/END on OSX which is CMD+LEFT, CMD+RIGHT
          if (e.keyCode == KEY.LEFT || e.keyCode == KEY.RIGHT || e.keyCode == KEY.HOME || e.keyCode == KEY.END) {
            // Defer execution to ensure carat pos has changed after HOME/END keys
            _.defer(resetBuffer);
    
    
            // IE9 doesn't fire the oninput event when backspace or delete is pressed. This causes the highlighting
            // to stay on the screen whenever backspace is pressed after a highlighed word. This is simply a hack
            // to force updateValues() to fire when backspace/delete is pressed in IE9.
            if (navigator.userAgent.indexOf("MSIE 9") > -1) {
              _.defer(updateValues);
            }
    
    
            return;
          }
    
          if (e.keyCode == KEY.BACKSPACE) {
            inputBuffer = inputBuffer.slice(0, -1 + inputBuffer.length); // Can't use splice, not available in IE
            return;
          }
    
          if (!elmAutocompleteList.is(':visible')) {
            return true;
          }
    
          switch (e.keyCode) {
            case KEY.UP:
            case KEY.DOWN:
              var elmCurrentAutoCompleteItem = null;
              if (e.keyCode == KEY.DOWN) {
                if (elmActiveAutoCompleteItem && elmActiveAutoCompleteItem.length) {
                  elmCurrentAutoCompleteItem = elmActiveAutoCompleteItem.next();
                } else {
                  elmCurrentAutoCompleteItem = elmAutocompleteList.find('li').first();
                }
              } else {
                elmCurrentAutoCompleteItem = $(elmActiveAutoCompleteItem).prev();
              }
    
              if (elmCurrentAutoCompleteItem.length) {
                selectAutoCompleteItem(elmCurrentAutoCompleteItem);
              }
    
              return false;
    
            case KEY.RETURN:
            case KEY.TAB:
              if (elmActiveAutoCompleteItem && elmActiveAutoCompleteItem.length) {
    
                elmActiveAutoCompleteItem.trigger('mousedown');
    
                return false;
              }
    
              break;
          }
    
          return true;
        }
    
        function hideAutoComplete() {
          elmActiveAutoCompleteItem = null;
          elmAutocompleteList.empty().hide();
        }
    
        function selectAutoCompleteItem(elmItem) {
          elmItem.addClass(settings.classes.autoCompleteItemActive);
          elmItem.siblings().removeClass(settings.classes.autoCompleteItemActive);
    
          elmActiveAutoCompleteItem = elmItem;
        }
    
        function populateDropdown(query, results) {
          elmAutocompleteList.show();
    
          // Filter items that has already been mentioned
    
          var mentionValues = _.pluck(mentionsCollection, 'value');
    
          results = _.reject(results, function (item) {
    
            return _.include(mentionValues, item.name);
    
          });
    
          if (!results.length) {
            hideAutoComplete();
            return;
          }
    
          elmAutocompleteList.empty();
          var elmDropDownList = $("<ul>").appendTo(elmAutocompleteList).hide();
    
          _.each(results, function (item, index) {
    
            var itemUid = _.uniqueId('mention_');
    
            autocompleteItemCollection[itemUid] = _.extend({}, item, {value: item.name});
    
    
            var elmListItem = $(settings.templates.autocompleteListItem({
              'id'      : utils.htmlEncode(item.id),
              'display' : utils.htmlEncode(item.name),
              'type'    : utils.htmlEncode(item.type),
              'content' : utils.highlightTerm(utils.htmlEncode((item.name)), query)
    
            })).attr('data-uid', itemUid);
    
            if (index === 0) {
              selectAutoCompleteItem(elmListItem);
    
            }
    
            if (settings.showAvatars) {
              var elmIcon;
    
              if (item.avatar) {
                elmIcon = $(settings.templates.autocompleteListItemAvatar({ avatar : item.avatar }));
              } else {
                elmIcon = $(settings.templates.autocompleteListItemIcon({ icon : item.icon }));
              }
              elmIcon.prependTo(elmListItem);
            }
            elmListItem = elmListItem.appendTo(elmDropDownList);
          });
    
          elmAutocompleteList.show();
          elmDropDownList.show();
        }
    
        function doSearch(query) {
          if (query && query.length && query.length >= settings.minChars) {
            settings.onDataRequest.call(this, 'search', query, function (responseData) {
              populateDropdown(query, responseData);
            });
          }
        }
    
    
        function resetInput() {
          elmInputBox.val('');
          mentionsCollection = [];
          updateValues();
        }
    
    
        // Public methods
        return {
    
          init : function (domTarget) {
    
            domInput = domTarget;
    
    
            initTextarea();
            initAutocomplete();
            initMentionsOverlay();
    
    Dan Hansen's avatar
    Dan Hansen a validé
    
    
            if( settings.prefillMention ) {
              addMention( settings.prefillMention );
    
    Dan Hansen's avatar
    Dan Hansen a validé
            }
    
          },
    
          val : function (callback) {
            if (!_.isFunction(callback)) {
              return;
            }
    
            var value = mentionsCollection.length ? elmInputBox.data('messageText') : getInputBoxValue();
            callback.call(this, value);
          },
    
          reset : function () {
    
          },
    
          getMentions : function (callback) {
            if (!_.isFunction(callback)) {
              return;
            }
    
            callback.call(this, mentionsCollection);
          }
        };
      };
    
      $.fn.mentionsInput = function (method, settings) {
    
    
        var outerArguments = arguments;
    
    
        if (typeof method === 'object' || !method) {
    
        }
    
        return this.each(function () {
    
          var instance = $.data(this, 'mentionsInput') || $.data(this, 'mentionsInput', new MentionsInput(settings));
    
    
          if (_.isFunction(instance[method])) {
            return instance[method].apply(this, Array.prototype.slice.call(outerArguments, 1));
    
          } else if (typeof method === 'object' || !method) {
    
            return instance.init.call(this, this);
    
    
          } else {
            $.error('Method ' + method + ' does not exist');
          }
    
        });
      };
    
    })(jQuery, _);
    
    Paul Hill's avatar
    Paul Hill a validé
    // @license-end