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

This commit is contained in:
Penar Musaraj 2019-10-11 11:37:44 -04:00 committed by GitHub
parent 9a81cb9e55
commit 3a469a79cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 237 additions and 25 deletions

View File

@ -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);
}
}
});

View File

@ -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"}}

View File

@ -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%;

View File

@ -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"

View File

@ -9,18 +9,13 @@ QUnit.test("add a hyperlink to a reply", async assert => {
await click(".topic-post:first-child button.reply");
await fillIn(".d-editor-input", "This is a link to ");
assert.equal(
find(".insert-link.modal-body").length,
0,
assert.ok(
!exists(".insert-link.modal-body"),
"no hyperlink modal by default"
);
await click(".d-editor button.link");
assert.equal(
find(".insert-link.modal-body").length,
1,
"hyperlink modal visible"
);
assert.ok(exists(".insert-link.modal-body"), "hyperlink modal visible");
await fillIn(".modal-body .link-url", "google.com");
await fillIn(".modal-body .link-text", "Google");
@ -32,9 +27,8 @@ QUnit.test("add a hyperlink to a reply", async assert => {
"adds link with url and text, prepends 'http://'"
);
assert.equal(
find(".insert-link.modal-body").length,
0,
assert.ok(
!exists(".insert-link.modal-body"),
"modal dismissed after submitting link"
);
@ -51,9 +45,8 @@ QUnit.test("add a hyperlink to a reply", async assert => {
"adds link with url and text, prepends 'http://'"
);
assert.equal(
find(".insert-link.modal-body").length,
0,
assert.ok(
!exists(".insert-link.modal-body"),
"modal dismissed after cancelling"
);
@ -70,4 +63,36 @@ QUnit.test("add a hyperlink to a reply", async assert => {
"[Reset](http://somelink.com) textarea contents.",
"adds link to a selected text"
);
await fillIn(".d-editor-input", "");
await click(".d-editor button.link");
await fillIn(".modal-body .link-url", "http://google.com");
await keyEvent(".modal-body .link-url", "keyup", 32);
assert.ok(
!exists(".internal-link-results"),
"does not show internal links search dropdown when inputting a url"
);
await fillIn(".modal-body .link-url", "local");
await keyEvent(".modal-body .link-url", "keyup", 32);
assert.ok(
exists(".internal-link-results"),
"shows internal links search dropdown when entering keywords"
);
await keyEvent(".insert-link", "keydown", 40);
await keyEvent(".insert-link", "keydown", 13);
assert.ok(
!exists(".internal-link-results"),
"search dropdown dismissed after selecting an internal link"
);
assert.ok(
find(".link-url")
.val()
.includes("http"),
"replaces link url field with internal link"
);
});