From e34960392cf93d5f71888980c6a04ee28d9ea9b2 Mon Sep 17 00:00:00 2001
From: Augier <contact@c-henry.fr>
Date: Sat, 20 Feb 2016 17:13:22 +0100
Subject: [PATCH] Code simplifications and typo

---
 .../app/views/publisher/mention_view.js       | 119 ++++++++++++------
 .../javascripts/app/views/search_base_view.js |  34 ++---
 .../javascripts/app/views/search_view.js      |   2 +-
 app/assets/javascripts/main.js                |   1 -
 app/assets/javascripts/mentions.js            |  48 -------
 .../app/views/publisher_mention_view_spec.js  |  11 +-
 .../app/views/search_base_view_spec.js        |  22 ++--
 .../javascripts/app/views/search_view_spec.js |   7 +-
 8 files changed, 118 insertions(+), 126 deletions(-)
 delete mode 100644 app/assets/javascripts/mentions.js

diff --git a/app/assets/javascripts/app/views/publisher/mention_view.js b/app/assets/javascripts/app/views/publisher/mention_view.js
index 4383da759c..27bb929905 100644
--- a/app/assets/javascripts/app/views/publisher/mention_view.js
+++ b/app/assets/javascripts/app/views/publisher/mention_view.js
@@ -32,6 +32,27 @@ app.views.PublisherMention = app.views.SearchBase.extend({
     "paste #status_message_fake_text": "onInputBoxPaste"
   },
 
+  /**
+   * Performs setup of the setup of the plugin.
+   *
+   * this.mentionsCollection: used to keep track of the people mentionned in the post
+   * this.inputBuffer: buffer to keep track of the text currently being typed. It is cleared
+   *                   each time a mention has been processed.
+   *                   See this#onInputBoxKeyPress
+   * this.currentDataQuery: contains the query for the search engine
+   *
+   * The plugin initilizes two different elements that will contain the text of the post:
+   *
+   * this.elmInputBox: hidden element which keeps track of typed text formatted following
+   *                   the mentioning syntax given by this.settings.templates#mentionItemSyntax
+   *                   For instance, if the user writes the text "Hello @user1", the resulting hidden
+   *                   text will be: "Hello @{user1 ; user1@pod.tld}. This is the text that is submitted
+   *                   to the pod when the user posts.
+   * this.elmMentionsOverlay: contains the text that will be displayed to the user
+   *
+   * this.mentionChar is a invisible caracter used to mark the name of the mentionned person
+   * during the process. See this#processMention
+   */
   initialize: function(){
     this.mentionsCollection = [];
     this.inputBuffer = [];
@@ -39,28 +60,32 @@ app.views.PublisherMention = app.views.SearchBase.extend({
     this.mentionChar = "\u200B";
 
     this.elmInputBox = this.$el.find("#status_message_fake_text");
-    this.elmInputWrapper = this.elmInputBox.parent();
-    this.elmWrapperBox = $(this.settings.templates.wrapper());
-    this.elmInputBox.wrapAll(this.elmWrapperBox);
-    this.elmWrapperBox = this.elmInputWrapper.find("> div").first();
+    var elmInputWrapper = this.elmInputBox.parent();
+    this.elmInputBox.wrapAll($(this.settings.templates.wrapper()));
+    var elmWrapperBox = elmInputWrapper.find("> div").first();
     this.elmMentionsOverlay = $(this.settings.templates.mentionsOverlay());
-    this.elmMentionsOverlay.prependTo(this.elmWrapperBox);
+    this.elmMentionsOverlay.prependTo(elmWrapperBox);
 
-    this.bindMentionningEvents();
-    this.completeSetup(this.getTypeaheadInput());
+    this.bindMentioningEvents();
+    app.views.SearchBase.prototype.initialize.call(this, {typeaheadElement: this.getTypeaheadInput()});
 
     this.$el.find(".twitter-typeahead").css({position: "absolute", left: "-1px"});
     this.$el.find(".twitter-typeahead .tt-menu").css("margin-top", 0);
   },
 
-  bindMentionningEvents: function(){
+  /**
+   * Attach events to Typeahead.
+   */
+  bindMentioningEvents: function(){
     var self = this;
+    // Process mention when the user selects a result.
     this.getTypeaheadInput().on("typeahead:select", function(evt, datum){
       self.processMention(datum);
       self.resetMentionBox();
       self.addToFilteredResults(datum);
     });
 
+    // Highlight the first result when the results dropdown opens
     this.getTypeaheadInput().on("typeahead:render", function(){
       self.select(self.$(".tt-menu .tt-suggestion").first());
     });
@@ -70,6 +95,10 @@ app.views.PublisherMention = app.views.SearchBase.extend({
     this.inputBuffer.length = 0;
   },
 
+  /**
+   * Cleans the collection of mentionned people. Rejects every item who's name
+   * is not present in the post an falsy values (false, null, "", etc.)
+   */
   updateMentionsCollection: function(){
     var inputText = this.getInputBoxValue();
 
@@ -79,6 +108,11 @@ app.views.PublisherMention = app.views.SearchBase.extend({
     this.mentionsCollection = _.compact(this.mentionsCollection);
   },
 
+  /**
+   * Adds mention to the mention collection
+   * @param person Mentionned person.
+   * JSON object of form { handle: <diaspora handle>, name: <name>, ... }
+   */
   addMention: function(person){
     if(!person || !person.name || !person.handle){
       return;
@@ -90,12 +124,25 @@ app.views.PublisherMention = app.views.SearchBase.extend({
     this.mentionsCollection.push(person);
   },
 
+  /**
+   * Process the text to add mention to the post. Every @mention in the text
+   * will be replaced by this.mentionChar + mention.name. This temporary text
+   * will then be replaced by final syntax in this#updateValues
+   *
+   * For instance if the user types text "Hello @use" and selects result user1,
+   * The text will be transformed to "Hello \u200Buser1" before calling this#updateValues
+   *
+   * @param mention Mentionned person.
+   * JSON object of form { handle: <diaspora handle>, name: <name>, ... }
+   */
   processMention: function(mention){
     var currentMessage = this.getInputBoxValue();
 
     var currentCaretPosition = this.getCaretPosition();
     var startCaretPosition = currentCaretPosition - (this.currentDataQuery.length + 1);
 
+    // Extracts the text before the mention and the text after it.
+    // startEndIndex is the position where to place the caret at the en of the process
     var start = currentMessage.substr(0, startCaretPosition);
     var end = currentMessage.substr(currentCaretPosition, currentMessage.length);
     var startEndIndex = (start + mention.name).length + 1;
@@ -107,16 +154,24 @@ app.views.PublisherMention = app.views.SearchBase.extend({
     this.currentDataQuery = "";
     this.resetMentionBox();
 
-    // Mentions & syntax message
+    // Autocompletes mention and updates message text
     var updatedMessageText = start + this.mentionChar + mention.name + end;
     this.elmInputBox.val(updatedMessageText);
     this.updateValues();
 
-    // Set correct focus and selection
+    // Set correct focus and caret position
     this.elmInputBox.focus();
     this.setCaretPosition(startEndIndex);
   },
 
+  /**
+   * Replaces every combination of this.mentionChar + mention.name by the
+   * correct syntax for both hidden text and visible one.
+   *
+   * For instance, the text "Hello \u200Buser1" will be tranformed to
+   * "Hello @{user1 ; user1@pod.tld}" in the hidden element and
+   * "Hello <strong><span>user1</span></strong>" in the element visible to the user.
+   */
   updateValues: function(){
     var syntaxMessage = this.getInputBoxValue();
     var mentionText = this.getInputBoxValue();
@@ -162,34 +217,21 @@ app.views.PublisherMention = app.views.SearchBase.extend({
     });
   },
 
-  selectNextResult: function(evt){
-    if(this.isVisible()){
-      evt.preventDefault();
-      evt.stopPropagation();
-    }
-
-    if(this.getSelected().size() !== 1 || this.getSelected().next().size() !== 1){
-      this.getSelected().removeClass("tt-cursor");
-      this.$el.find(".tt-suggestion").first().addClass("tt-cursor");
-    }
-    else{
-      this.getSelected().removeClass("tt-cursor").next().addClass("tt-cursor");
+  /**
+   * Selects next or previous result when result dropdown is open and
+   * user press up and down arrows.
+   */
+  onArrowKeysPress: function(e){
+    if(!this.isVisible() || (e.keyCode !== this.KEYS.UP && e.keyCode !== this.KEYS.DOWN)){
+      return;
     }
-  },
 
-  selectPreviousResult: function(evt){
-    if(this.isVisible()){
-      evt.preventDefault();
-      evt.stopPropagation();
-    }
+    e.preventDefault();
+    e.stopPropagation();
 
-    if(this.getSelected().size() !== 1 || this.getSelected().prev().size() !== 1){
-      this.getSelected().removeClass("tt-cursor");
-      this.$el.find(".tt-suggestion").last().addClass("tt-cursor");
-    }
-    else{
-      this.getSelected().removeClass("tt-cursor").prev().addClass("tt-cursor");
-    }
+    this.getTypeaheadInput().typeahead("activate");
+    this.getTypeaheadInput().typeahead("open");
+    this.getTypeaheadInput().trigger($.Event("keydown", {keyCode: e.keyCode}));
   },
 
   onInputBoxKeyPress: function(e){
@@ -200,6 +242,9 @@ app.views.PublisherMention = app.views.SearchBase.extend({
     }
   },
 
+  /**
+   * Listens for user input and opens results dropdown when input contains the trigger char
+   */
   onInputBoxInput: function(){
     this.updateValues();
     this.updateMentionsCollection();
@@ -244,10 +289,8 @@ app.views.PublisherMention = app.views.SearchBase.extend({
         this.resetMentionBox();
         break;
       case this.KEYS.UP:
-        this.selectPreviousResult(e);
-        break;
       case this.KEYS.DOWN:
-        this.selectNextResult(e);
+        this.onArrowKeysPress(e);
         break;
       case this.KEYS.RETURN:
       case this.KEYS.TAB:
diff --git a/app/assets/javascripts/app/views/search_base_view.js b/app/assets/javascripts/app/views/search_base_view.js
index 65bf83fcd7..6b2dfae297 100644
--- a/app/assets/javascripts/app/views/search_base_view.js
+++ b/app/assets/javascripts/app/views/search_base_view.js
@@ -1,6 +1,6 @@
 app.views.SearchBase = app.views.Base.extend({
-  completeSetup: function(typeaheadElement){
-    this.typeaheadElement = $(typeaheadElement);
+  initialize: function(options){
+    this.typeaheadElement = $(options.typeaheadElement);
     this.setupBloodhound();
     this.setupTypeahead();
     this.bindSelectionEvents();
@@ -59,21 +59,21 @@ app.views.SearchBase = app.views.Base.extend({
 
   setupTypeahead: function() {
     this.typeaheadElement.typeahead({
-          hint: false,
-          highlight: true,
-          minLength: 2
-        },
-        {
-          name: "search",
-          display: "name",
-          limit: 5,
-          source: this.bloodhound.customSearch,
-          templates: {
-            /* jshint camelcase: false */
-            suggestion: HandlebarsTemplates.search_suggestion_tpl
-            /* jshint camelcase: true */
-          }
-        });
+      hint: false,
+      highlight: true,
+      minLength: 2
+    },
+    {
+      name: "search",
+      display: "name",
+      limit: 5,
+      source: this.searchFormAction !== undefined ? this.bloodhound : this.bloodhound.customSearch,
+      templates: {
+        /* jshint camelcase: false */
+        suggestion: HandlebarsTemplates.search_suggestion_tpl
+        /* jshint camelcase: true */
+      }
+    });
   },
 
   transformBloodhoundResponse: function(response) {
diff --git a/app/assets/javascripts/app/views/search_view.js b/app/assets/javascripts/app/views/search_view.js
index 65ed775c0d..ec183b0945 100644
--- a/app/assets/javascripts/app/views/search_view.js
+++ b/app/assets/javascripts/app/views/search_view.js
@@ -8,7 +8,7 @@ app.views.Search = app.views.SearchBase.extend({
 
   initialize: function(){
     this.searchFormAction = this.$el.attr("action");
-    this.completeSetup(this.getTypeaheadElement());
+    app.views.SearchBase.prototype.initialize.call(this, {typeaheadElement: this.getTypeaheadElement()});
     this.bindMoreSelectionEvents();
     this.getTypeaheadElement().on("typeahead:select", this.suggestionSelected);
   },
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 458824bd1e..2a2ef7c1db 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -38,7 +38,6 @@
 //= require_tree ./helpers
 //= require_tree ./pages
 //= require_tree ./widgets
-//= require mentions
 //= require bootstrap
 //= require osmlocator
 //= require bootstrap-switch
diff --git a/app/assets/javascripts/mentions.js b/app/assets/javascripts/mentions.js
deleted file mode 100644
index f805b97fae..0000000000
--- a/app/assets/javascripts/mentions.js
+++ /dev/null
@@ -1,48 +0,0 @@
-// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
-
-var Mentions = {
-  initialize: function(mentionsInput) {
-    return mentionsInput.mentionsInput(Mentions.options);
-  },
-
-  // pre-fetch the list of contacts for the current user.
-  // called by the initializer of the publisher, for faster ('offline')
-  // execution of the filtering for mentions
-  fetchContacts : function(){
-    Mentions.contacts || $.getJSON("/contacts", function(data) {
-      Mentions.contacts = Mentions.createList(data);
-    });
-  },
-
-  // creates a list of mentions out of a list of contacts
-  // @see _contactToMention
-  createList: function(contacts) {
-    return _.map(contacts, Mentions._contactToMention);
-  },
-
-  // takes a given contact object and modifies to fit the format
-  // expected by the jQuery.mentionsInput plugin.
-  // @see http://podio.github.com/jquery-mentions-input/
-  _contactToMention: function(contact) {
-    contact.value = contact.name;
-    return contact;
-  },
-
-  // default options for jQuery.mentionsInput
-  // @see http://podio.github.com/jquery-mentions-input/
-  options: {
-    elastic: false,
-    minChars: 1,
-
-    onDataRequest: function(mode, query, callback) {
-      var filteredResults = _.filter(Mentions.contacts, function(item) { return item.name.toLowerCase().indexOf(query.toLowerCase()) > -1 });
-
-      callback.call(this, filteredResults.slice(0,5));
-    },
-
-    templates: {
-      mentionItemSyntax: _.template("@{<%= name %> ; <%= handle %>}")
-    }
-  }
-};
-// @license-end
diff --git a/spec/javascripts/app/views/publisher_mention_view_spec.js b/spec/javascripts/app/views/publisher_mention_view_spec.js
index f104704942..f67aad2159 100644
--- a/spec/javascripts/app/views/publisher_mention_view_spec.js
+++ b/spec/javascripts/app/views/publisher_mention_view_spec.js
@@ -8,8 +8,8 @@ describe("app.views.PublisherMention", function(){
 
   describe("initialize", function(){
     beforeEach(function(){
-      spyOn(app.views.PublisherMention.prototype, "completeSetup").and.callThrough();
-      spyOn(app.views.PublisherMention.prototype, "bindMentionningEvents").and.callThrough();
+      spyOn(app.views.SearchBase.prototype, "initialize").and.callThrough();
+      spyOn(app.views.PublisherMention.prototype, "bindMentioningEvents").and.callThrough();
       this.view = new app.views.PublisherMention({ el: "#publisher" });
     });
 
@@ -21,8 +21,9 @@ describe("app.views.PublisherMention", function(){
     });
 
     it("calls completeSetup", function(){
-      expect(app.views.PublisherMention.prototype.completeSetup).toHaveBeenCalledWith(this.view.getTypeaheadInput());
-      expect(app.views.PublisherMention.prototype.bindMentionningEvents).toHaveBeenCalled();
+      expect(app.views.SearchBase.prototype.initialize)
+        .toHaveBeenCalledWith({typeaheadElement: this.view.getTypeaheadInput()});
+      expect(app.views.PublisherMention.prototype.bindMentioningEvents).toHaveBeenCalled();
     });
 
     it("initializes html elements", function(){
@@ -33,7 +34,7 @@ describe("app.views.PublisherMention", function(){
     });
   });
 
-  describe("bindMentionningEvents", function(){
+  describe("bindMentioningEvents", function(){
     beforeEach(function(){
       spyOn(app.views.PublisherMention.prototype, "processMention");
       spyOn(app.views.PublisherMention.prototype, "resetMentionBox");
diff --git a/spec/javascripts/app/views/search_base_view_spec.js b/spec/javascripts/app/views/search_base_view_spec.js
index 36bd65912b..d0e633305b 100644
--- a/spec/javascripts/app/views/search_base_view_spec.js
+++ b/spec/javascripts/app/views/search_base_view_spec.js
@@ -1,36 +1,32 @@
 describe("app.views.SearchBase", function() {
   beforeEach(function(){
     spec.content().html(
-      "<form action='/search' id='search_people_form'><input id='q' name='q' type='search'></input></form>"
+      "<form action='/search' id='search_people_form'><input id='q' name='q' type='search'/></form>"
     );
   });
 
-  describe("completeSetup", function(){
+  describe("initialize", function(){
     it("calls setupBloodhound", function(){
       spyOn(app.views.SearchBase.prototype, "setupBloodhound").and.callThrough();
-      var view = new app.views.SearchBase({el: "#search_people_form"});
-      view.completeSetup();
+      new app.views.SearchBase({el: "#search_people_form"});
       expect(app.views.SearchBase.prototype.setupBloodhound).toHaveBeenCalled();
     });
 
     it("calls setupTypeahead", function(){
       spyOn(app.views.SearchBase.prototype, "setupTypeahead");
-      var view = new app.views.SearchBase({el: "#search_people_form"});
-      view.completeSetup();
+      new app.views.SearchBase({el: "#search_people_form"});
       expect(app.views.SearchBase.prototype.setupTypeahead).toHaveBeenCalled();
     });
 
     it("calls bindSelectionEvents", function(){
       spyOn(app.views.SearchBase.prototype, "bindSelectionEvents");
-      var view = new app.views.SearchBase({el: "#search_people_form"});
-      view.completeSetup();
+      new app.views.SearchBase({el: "#search_people_form"});
       expect(app.views.SearchBase.prototype.bindSelectionEvents).toHaveBeenCalled();
     });
 
     it("initializes the results to filter", function(){
       spyOn(app.views.SearchBase.prototype, "bindSelectionEvents");
       var view = new app.views.SearchBase({el: "#search_people_form"});
-      view.completeSetup();
       expect(view.resultsTofilter.length).toBe(0);
     });
   });
@@ -44,7 +40,7 @@ describe("app.views.SearchBase", function() {
 
     context("when performing a local search with 1 filtered result", function(){
       beforeEach(function(){
-        this.view.completeSetup(this.view.$("#q"));
+        this.view.initialize({typeaheadElement: this.view.$("#q")});
         this.view.bloodhound.add([
           {"id":1,"guid":"1","name":"user1","handle":"user1@pod.tld","url":"/people/1"},
           {"id":2,"guid":"2","name":"user2","handle":"user2@pod.tld","url":"/people/2"}
@@ -106,7 +102,7 @@ describe("app.views.SearchBase", function() {
   describe("bindSelectionEvents", function(){
     beforeEach(function() {
       this.view = new app.views.SearchBase({ el: "#search_people_form" });
-      this.view.completeSetup(this.view.$("#q"));
+      this.view.initialize({typeaheadElement: this.view.$("#q")});
       this.view.bloodhound.add([
         {"person": true, "name":"user1", "handle":"user1@pod.tld"},
         {"person": true, "name":"user2", "handle":"user2@pod.tld"}
@@ -147,7 +143,7 @@ describe("app.views.SearchBase", function() {
   describe("addToFilteredResults", function(){
     beforeEach(function() {
       this.view = new app.views.SearchBase({ el: "#search_people_form" });
-      this.view.completeSetup(this.view.$("#q"));
+      this.view.initialize({typeaheadElement: this.view.$("#q")});
     });
 
     context("when item is a person", function(){
@@ -168,7 +164,7 @@ describe("app.views.SearchBase", function() {
   describe("clearFilteredResults", function(){
     beforeEach(function() {
       this.view = new app.views.SearchBase({ el: "#search_people_form" });
-      this.view.completeSetup(this.view.$("#q"));
+      this.view.initialize({typeaheadElement: this.view.$("#q")});
     });
 
     context("clear filtered results", function(){
diff --git a/spec/javascripts/app/views/search_view_spec.js b/spec/javascripts/app/views/search_view_spec.js
index 70525ae3bb..001fabddde 100644
--- a/spec/javascripts/app/views/search_view_spec.js
+++ b/spec/javascripts/app/views/search_view_spec.js
@@ -6,10 +6,11 @@ describe("app.views.Search", function(){
   });
 
   describe("initialize", function(){
-    it("calls completeSetup", function(){
-      spyOn(app.views.Search.prototype, "completeSetup").and.callThrough();
+    it("calls app.views.SearchBase.prototype.initialize", function(){
+      spyOn(app.views.SearchBase.prototype, "initialize").and.callThrough();
       var view = new app.views.Search({el: "#search_people_form"});
-      expect(app.views.Search.prototype.completeSetup).toHaveBeenCalledWith(view.getTypeaheadElement());
+      expect(app.views.SearchBase.prototype.initialize)
+        .toHaveBeenCalledWith({typeaheadElement: view.getTypeaheadElement()});
     });
 
     it("calls bindMoreSelectionEvents", function(){
-- 
GitLab