FEATURE: search topics when adding a link in composer (#8178)

FEATURE: search topics when adding a link in composer (#8178)

diff --git a/app/assets/javascripts/discourse/controllers/insert-hyperlink.js.es6 b/app/assets/javascripts/discourse/controllers/insert-hyperlink.js.es6
index d4f15d9..313ab33 100644
--- a/app/assets/javascripts/discourse/controllers/insert-hyperlink.js.es6
+++ b/app/assets/javascripts/discourse/controllers/insert-hyperlink.js.es6
@@ -1,15 +1,140 @@
 import ModalFunctionality from "discourse/mixins/modal-functionality";
+import { searchForTerm } from "discourse/lib/search";
 
 export default Ember.Controller.extend(ModalFunctionality, {
-  linkUrl: "",
-  linkText: "",
+  _debounced: null,
+  _activeSearch: null,
 
   onShow() {
-    Ember.run.next(() =>
-      $(this)
-        .find("input.link-url")
-        .focus()
-    );
+    this.setProperties({
+      linkUrl: "",
+      linkText: "",
+      searchResults: [],
+      searchLoading: false,
+      selectedRow: -1
+    });
+
+    Ember.run.scheduleOnce("afterRender", () => {
+      const element = document.querySelector(".insert-link");
+
+      element.addEventListener("keydown", e => this.keyDown(e));
+
+      element
+        .closest(".modal-inner-container")
+        .addEventListener("mousedown", e => this.mouseDown(e));
+
+      document.querySelector("input.link-url").focus();
+    });
+  },
+
+  keyDown(e) {
+    switch (e.which) {
+      case 40:
+        this.highlightRow(e, "down");
+        break;
+      case 38:
+        this.highlightRow(e, "up");
+        break;
+      case 13:
+        // override Enter behaviour when a row is selected
+        if (this.selectedRow > -1) {
+          const selected = document.querySelectorAll(
+            ".internal-link-results .search-link"
+          )[this.selectedRow];
+          this.selectLink(selected);
+          e.preventDefault();
+          e.stopPropagation();
+        }
+        break;
+      case 27:
+        // Esc should cancel dropdown first
+        if (this.searchResults.length) {
+          this.set("searchResults", []);
+          e.preventDefault();
+          e.stopPropagation();
+        }
+        break;
+    }
+  },
+
+  mouseDown(e) {
+    if (!e.target.closest(".inputs")) {
+      this.set("searchResults", []);
+    }
+  },
+
+  highlightRow(e, direction) {
+    const index =
+      direction === "down" ? this.selectedRow + 1 : this.selectedRow - 1;
+
+    if (index > -1 && index < this.searchResults.length) {
+      document
+        .querySelectorAll(".internal-link-results .search-link")
+        [index].focus();
+      this.set("selectedRow", index);
+    } else {
+      this.set("selectedRow", -1);
+      document.querySelector("input.link-url").focus();
+    }
+
+    e.preventDefault();
+  },
+
+  selectLink(el) {
+    this.setProperties({
+      linkUrl: el.href,
+      searchResults: [],
+      selectedRow: -1
+    });
+
+    if (!this.linkText && el.dataset.title) {
+      this.set("linkText", el.dataset.title);
+    }
+
+    document.querySelector("input.link-text").focus();
+  },
+
+  triggerSearch() {
+    if (this.linkUrl.length > 3 && this.linkUrl.indexOf("http") === -1) {
+      this.set("searchLoading", true);
+      this._activeSearch = searchForTerm(this.linkUrl, {
+        typeFilter: "topic"
+      });
+      this._activeSearch
+        .then(results => {
+          if (results && results.topics && results.topics.length > 0) {
+            this.set("searchResults", results.topics);
+          } else {
+            this.set("searchResults", []);
+          }
+        })
+        .finally(() => {
+          this.set("searchLoading", false);
+          this._activeSearch = null;
+        });
+    } else {
+      this.abortSearch();
+    }
+  },
+
+  abortSearch() {
+    if (this._activeSearch) {
+      this._activeSearch.abort();
+    }
+    this.setProperties({
+      searchResults: [],
+      searchLoading: false
+    });
+  },
+
+  onClose() {
+    const element = document.querySelector(".insert-link");
+    element.removeEventListener("keydown", this.keyDown);
+    element
+      .closest(".modal-inner-container")
+      .removeEventListener("mousedown", this.mouseDown);
+
+    Ember.run.cancel(this._debounced);
   },
 
   actions: {
@@ -35,12 +160,20 @@ export default Ember.Controller.extend(ModalFunctionality, {
           this.toolbarEvent.selectText(sel.start + 1, origLink.length);
         }
       }
-      this.set("linkUrl", "");
-      this.set("linkText", "");
       this.send("closeModal");
     },
     cancel() {
       this.send("closeModal");
+    },
+    linkClick(e) {
+      if (!e.metaKey && !e.ctrlKey) {
+        e.preventDefault();
+        e.stopPropagation();
+        this.selectLink(e.target);
+      }
+    },
+    search() {
+      this._debounced = Ember.run.debounce(this, this.triggerSearch, 400);
     }
   }
 });
diff --git a/app/assets/javascripts/discourse/templates/modal/insert-hyperlink.hbs b/app/assets/javascripts/discourse/templates/modal/insert-hyperlink.hbs
index 8aa0607..ec15a2b 100644
--- a/app/assets/javascripts/discourse/templates/modal/insert-hyperlink.hbs
+++ b/app/assets/javascripts/discourse/templates/modal/insert-hyperlink.hbs
@@ -1,6 +1,27 @@
 {{#d-modal-body title="composer.link_dialog_title" class="insert-link"}}
     <div class="inputs">
-      {{text-field value=linkUrl placeholderKey="composer.link_url_placeholder" class="link-url"}}
+      {{text-field
+        value=linkUrl
+        placeholderKey="composer.link_url_placeholder"
+        class="link-url"
+        key-up=(action "search")
+      }}
+      {{#if searchLoading}}
+        {{loading-spinner}}
+      {{/if}}
+      {{#if searchResults}}
+        <div class="internal-link-results">
+          {{#each searchResults as |r index|}}
+            <a
+              class="search-link"
+              href="{{r.url}}"
+              onclick={{action "linkClick"}}
+              data-title="{{r.title}}">
+              {{replace-emoji r.fancy_title}}
+            </a>
+          {{/each}}
+        </div>
+      {{/if}}
     </div>
     <div class="inputs">
       {{text-field value=linkText placeholderKey="composer.link_optional_text" class="link-text"}}
diff --git a/app/assets/stylesheets/common/base/modal.scss b/app/assets/stylesheets/common/base/modal.scss
index bf181bf..44f7c2f 100644
--- a/app/assets/stylesheets/common/base/modal.scss
+++ b/app/assets/stylesheets/common/base/modal.scss
@@ -201,9 +201,42 @@
     }
 
     &.insert-link {
+      overflow-y: visible;
       input {
         min-width: 300px;
       }
+
+      .inputs {
+        position: relative;
+        .spinner {
+          position: absolute;
+          right: 8px;
+          top: -15px;
+          width: 10px;
+          height: 10px;
+        }
+        .internal-link-results {
+          position: absolute;
+          top: 70%;
+          padding: 5px 10px;
+          box-shadow: shadow("card");
+          z-index: 5;
+          background-color: $secondary;
+          max-height: 150px;
+          width: 90%;
+          overflow-y: auto;
+          > a {
+            padding: 6px;
+            border-bottom: 1px solid $primary-low;
+            cursor: pointer;
+            display: block;
+            &:hover,
+            &:focus {
+              background-color: $highlight-medium;
+            }
+          }
+        }
+      }
     }
     textarea {
       width: 99%;
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index f30f108..df10b60 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -1685,7 +1685,7 @@ en:
       link_description: "enter link description here"
       link_dialog_title: "Insert Hyperlink"
       link_optional_text: "optional title"
-      link_url_placeholder: "https://example.com"
+      link_url_placeholder: "Paste a URL or type to search topics"
       quote_title: "Blockquote"
       quote_text: "Blockquote"
       code_title: "Preformatted text"
diff --git a/test/javascripts/acceptance/composer-hyperlink-test.js.es6 b/test/javascripts/acceptance/composer-hyperlink-test.js.es6

[... diff too long, it was truncated ...]

GitHub sha: 3a469a79

2 Likes

Very cool feature :heart: