DEV: Migrate insert-hyperlink to the new modal api (#23051)
This commit is contained in:
parent
c280c1c52b
commit
bc26e6c4b2
|
@ -32,7 +32,6 @@ import { loadOneboxes } from "discourse/lib/load-oneboxes";
|
||||||
import loadScript from "discourse/lib/load-script";
|
import loadScript from "discourse/lib/load-script";
|
||||||
import { resolveCachedShortUrls } from "pretty-text/upload-short-url";
|
import { resolveCachedShortUrls } from "pretty-text/upload-short-url";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import showModal from "discourse/lib/show-modal";
|
|
||||||
import { siteDir } from "discourse/lib/text-direction";
|
import { siteDir } from "discourse/lib/text-direction";
|
||||||
import { translations } from "pretty-text/emoji/data";
|
import { translations } from "pretty-text/emoji/data";
|
||||||
import { wantsNewWindow } from "discourse/lib/intercept-click";
|
import { wantsNewWindow } from "discourse/lib/intercept-click";
|
||||||
|
@ -40,6 +39,7 @@ import { action, computed } from "@ember/object";
|
||||||
import TextareaTextManipulation, {
|
import TextareaTextManipulation, {
|
||||||
getHead,
|
getHead,
|
||||||
} from "discourse/mixins/textarea-text-manipulation";
|
} from "discourse/mixins/textarea-text-manipulation";
|
||||||
|
import InsertHyperlink from "discourse/components/modal/insert-hyperlink";
|
||||||
|
|
||||||
function getButtonLabel(labelKey, defaultLabel) {
|
function getButtonLabel(labelKey, defaultLabel) {
|
||||||
// use the Font Awesome icon if the label matches the default
|
// use the Font Awesome icon if the label matches the default
|
||||||
|
@ -218,6 +218,9 @@ export function onToolbarCreate(func) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Component.extend(TextareaTextManipulation, {
|
export default Component.extend(TextareaTextManipulation, {
|
||||||
|
emojiStore: service("emoji-store"),
|
||||||
|
modal: service(),
|
||||||
|
|
||||||
classNames: ["d-editor"],
|
classNames: ["d-editor"],
|
||||||
ready: false,
|
ready: false,
|
||||||
lastSel: null,
|
lastSel: null,
|
||||||
|
@ -225,7 +228,6 @@ export default Component.extend(TextareaTextManipulation, {
|
||||||
showLink: true,
|
showLink: true,
|
||||||
emojiPickerIsActive: false,
|
emojiPickerIsActive: false,
|
||||||
emojiFilter: "",
|
emojiFilter: "",
|
||||||
emojiStore: service("emoji-store"),
|
|
||||||
isEditorFocused: false,
|
isEditorFocused: false,
|
||||||
processPreview: true,
|
processPreview: true,
|
||||||
composerFocusSelector: "#reply-control .d-editor-input",
|
composerFocusSelector: "#reply-control .d-editor-input",
|
||||||
|
@ -770,9 +772,11 @@ export default Component.extend(TextareaTextManipulation, {
|
||||||
linkText = this._lastSel.value;
|
linkText = this._lastSel.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
showModal("insert-hyperlink").setProperties({
|
this.modal.show(InsertHyperlink, {
|
||||||
linkText,
|
model: {
|
||||||
toolbarEvent,
|
linkText,
|
||||||
|
toolbarEvent,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
{{! template-lint-disable no-pointer-down-event-binding }}
|
||||||
|
<DModal
|
||||||
|
{{on "keydown" this.keyDown}}
|
||||||
|
{{on "mousedown" this.mouseDown}}
|
||||||
|
@closeModal={{@closeModal}}
|
||||||
|
@title={{i18n "composer.link_dialog_title"}}
|
||||||
|
@bodyClass="insert-link"
|
||||||
|
class="insert-hyperlink-modal"
|
||||||
|
>
|
||||||
|
<:body>
|
||||||
|
<div class="inputs">
|
||||||
|
<input
|
||||||
|
{{on "input" this.search}}
|
||||||
|
value={{this.linkUrl}}
|
||||||
|
placeholder={{i18n "composer.link_url_placeholder"}}
|
||||||
|
type="text"
|
||||||
|
autofocus="autofocus"
|
||||||
|
class="link-url"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{{#if this.searchLoading}}
|
||||||
|
{{loading-spinner}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.searchResults}}
|
||||||
|
<div class="internal-link-results">
|
||||||
|
{{#each this.searchResults as |result|}}
|
||||||
|
<a
|
||||||
|
{{on "click" this.linkClick}}
|
||||||
|
href={{result.url}}
|
||||||
|
data-title={{result.fancy_title}}
|
||||||
|
class="search-link"
|
||||||
|
>
|
||||||
|
<TopicStatus @topic={{result}} @disableActions={{true}} />
|
||||||
|
{{replace-emoji result.title}}
|
||||||
|
<div class="search-category">
|
||||||
|
{{#if result.category.parentCategory}}
|
||||||
|
{{category-link result.category.parentCategory}}
|
||||||
|
{{/if}}
|
||||||
|
{{category-link result.category hideParent=true}}
|
||||||
|
{{discourse-tags result}}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inputs">
|
||||||
|
<input
|
||||||
|
{{on "input" this.updateLinkText}}
|
||||||
|
value={{this.linkText}}
|
||||||
|
placeholder={{i18n "composer.link_optional_text"}}
|
||||||
|
type="text"
|
||||||
|
class="link-text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</:body>
|
||||||
|
|
||||||
|
<:footer>
|
||||||
|
<DButton
|
||||||
|
@action={{this.ok}}
|
||||||
|
@label="composer.modal_ok"
|
||||||
|
type="submit"
|
||||||
|
class="btn-primary"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DButton
|
||||||
|
@action={{@closeModal}}
|
||||||
|
@label="composer.modal_cancel"
|
||||||
|
class="btn-danger"
|
||||||
|
/>
|
||||||
|
</:footer>
|
||||||
|
</DModal>
|
|
@ -0,0 +1,164 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { cancel } from "@ember/runloop";
|
||||||
|
import discourseDebounce from "discourse-common/lib/debounce";
|
||||||
|
import { isEmpty } from "@ember/utils";
|
||||||
|
import { prefixProtocol } from "discourse/lib/url";
|
||||||
|
import { searchForTerm } from "discourse/lib/search";
|
||||||
|
|
||||||
|
export default class InsertHyperlink extends Component {
|
||||||
|
@tracked linkText = this.args.model.linkText;
|
||||||
|
@tracked linkUrl = "";
|
||||||
|
@tracked selectedRow = -1;
|
||||||
|
@tracked searchResults = [];
|
||||||
|
@tracked searchLoading = false;
|
||||||
|
_debounced;
|
||||||
|
_activeSearch;
|
||||||
|
|
||||||
|
willDestroy() {
|
||||||
|
super.willDestroy(...arguments);
|
||||||
|
cancel(this._debounced);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.selectedRow = index;
|
||||||
|
} else {
|
||||||
|
this.selectedRow = -1;
|
||||||
|
document.querySelector("input.link-url").focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
selectLink(el) {
|
||||||
|
this.searchResults = [];
|
||||||
|
this.linkUrl = el.href;
|
||||||
|
this.selectedRow = -1;
|
||||||
|
|
||||||
|
if (!this.linkText && el.dataset.title) {
|
||||||
|
this.linkText = el.dataset.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector("input.link-text").focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async triggerSearch() {
|
||||||
|
if (this.linkUrl.length < 4 || this.linkUrl.startsWith("http")) {
|
||||||
|
this.abortSearch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.searchLoading = true;
|
||||||
|
this._activeSearch = searchForTerm(this.linkUrl, {
|
||||||
|
typeFilter: "topic",
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await this._activeSearch;
|
||||||
|
this.searchResults = results?.topics || [];
|
||||||
|
} finally {
|
||||||
|
this.searchLoading = false;
|
||||||
|
this._activeSearch = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abortSearch() {
|
||||||
|
this._activeSearch?.abort();
|
||||||
|
|
||||||
|
this.searchResults = [];
|
||||||
|
this.searchLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
keyDown(event) {
|
||||||
|
switch (event.key) {
|
||||||
|
case "ArrowDown":
|
||||||
|
this.highlightRow(event, "down");
|
||||||
|
break;
|
||||||
|
case "ArrowUp":
|
||||||
|
this.highlightRow(event, "up");
|
||||||
|
break;
|
||||||
|
case "Enter":
|
||||||
|
// override Enter behavior when a row is selected
|
||||||
|
if (this.selectedRow > -1) {
|
||||||
|
const selected = document.querySelectorAll(
|
||||||
|
".internal-link-results .search-link"
|
||||||
|
)[this.selectedRow];
|
||||||
|
this.selectLink(selected);
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "Escape":
|
||||||
|
// Esc should cancel dropdown first
|
||||||
|
if (this.searchResults.length) {
|
||||||
|
this.searchResults = [];
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
} else {
|
||||||
|
this.args.closeModal();
|
||||||
|
document.querySelector(".d-editor-input")?.focus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
mouseDown(event) {
|
||||||
|
if (!event.target.closest(".inputs")) {
|
||||||
|
this.searchResults = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
ok() {
|
||||||
|
const origLink = this.linkUrl;
|
||||||
|
const linkUrl = prefixProtocol(origLink);
|
||||||
|
const sel = this.args.model.toolbarEvent.selected;
|
||||||
|
|
||||||
|
if (isEmpty(linkUrl)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkText = this.linkText || "";
|
||||||
|
|
||||||
|
if (linkText.length) {
|
||||||
|
this.args.model.toolbarEvent.addText(`[${linkText}](${linkUrl})`);
|
||||||
|
} else if (sel.value) {
|
||||||
|
this.args.model.toolbarEvent.addText(`[${sel.value}](${linkUrl})`);
|
||||||
|
} else {
|
||||||
|
this.args.model.toolbarEvent.addText(`[${origLink}](${linkUrl})`);
|
||||||
|
this.args.model.toolbarEvent.selectText(sel.start + 1, origLink.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.args.closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
linkClick(e) {
|
||||||
|
if (!e.metaKey && !e.ctrlKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.selectLink(e.target.closest(".search-link"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
updateLinkText(event) {
|
||||||
|
this.linkText = event.target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
search(event) {
|
||||||
|
this.linkUrl = event.target.value;
|
||||||
|
this._debounced = discourseDebounce(this, this.triggerSearch, 400);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,186 +0,0 @@
|
||||||
import { cancel, schedule } from "@ember/runloop";
|
|
||||||
import Controller from "@ember/controller";
|
|
||||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
|
||||||
import discourseDebounce from "discourse-common/lib/debounce";
|
|
||||||
import { isEmpty } from "@ember/utils";
|
|
||||||
import { prefixProtocol } from "discourse/lib/url";
|
|
||||||
import { searchForTerm } from "discourse/lib/search";
|
|
||||||
|
|
||||||
export default Controller.extend(ModalFunctionality, {
|
|
||||||
_debounced: null,
|
|
||||||
_activeSearch: null,
|
|
||||||
|
|
||||||
onShow() {
|
|
||||||
this.setProperties({
|
|
||||||
linkUrl: "",
|
|
||||||
linkText: "",
|
|
||||||
searchResults: [],
|
|
||||||
searchLoading: false,
|
|
||||||
selectedRow: -1,
|
|
||||||
});
|
|
||||||
|
|
||||||
schedule("afterRender", () => {
|
|
||||||
const element = document.querySelector(".insert-link");
|
|
||||||
element.addEventListener("keydown", this.keyDown);
|
|
||||||
|
|
||||||
element
|
|
||||||
.closest(".modal-inner-container")
|
|
||||||
.addEventListener("mousedown", this.mouseDown);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
@bind
|
|
||||||
keyDown(event) {
|
|
||||||
switch (event.which) {
|
|
||||||
case 40:
|
|
||||||
this.highlightRow(event, "down");
|
|
||||||
break;
|
|
||||||
case 38:
|
|
||||||
this.highlightRow(event, "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);
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 27:
|
|
||||||
// Esc should cancel dropdown first
|
|
||||||
if (this.searchResults.length) {
|
|
||||||
this.set("searchResults", []);
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
} else {
|
|
||||||
this.send("closeModal");
|
|
||||||
document.querySelector(".d-editor-input")?.focus();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
@bind
|
|
||||||
mouseDown(event) {
|
|
||||||
if (!event.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.startsWith("http")) {
|
|
||||||
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);
|
|
||||||
|
|
||||||
cancel(this._debounced);
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
ok() {
|
|
||||||
const origLink = this.linkUrl;
|
|
||||||
const linkUrl = prefixProtocol(origLink);
|
|
||||||
const sel = this.toolbarEvent.selected;
|
|
||||||
|
|
||||||
if (isEmpty(linkUrl)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const linkText = this.linkText || "";
|
|
||||||
|
|
||||||
if (linkText.length) {
|
|
||||||
this.toolbarEvent.addText(`[${linkText}](${linkUrl})`);
|
|
||||||
} else {
|
|
||||||
if (sel.value) {
|
|
||||||
this.toolbarEvent.addText(`[${sel.value}](${linkUrl})`);
|
|
||||||
} else {
|
|
||||||
this.toolbarEvent.addText(`[${origLink}](${linkUrl})`);
|
|
||||||
this.toolbarEvent.selectText(sel.start + 1, origLink.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.send("closeModal");
|
|
||||||
},
|
|
||||||
cancel() {
|
|
||||||
this.send("closeModal");
|
|
||||||
},
|
|
||||||
linkClick(e) {
|
|
||||||
if (!e.metaKey && !e.ctrlKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
this.selectLink(e.target.closest(".search-link"));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
search() {
|
|
||||||
this._debounced = discourseDebounce(this, this.triggerSearch, 400);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -33,7 +33,6 @@ const KNOWN_LEGACY_MODALS = [
|
||||||
"history",
|
"history",
|
||||||
"ignore-duration-with-username",
|
"ignore-duration-with-username",
|
||||||
"ignore-duration",
|
"ignore-duration",
|
||||||
"insert-hyperlink",
|
|
||||||
"jump-to-post",
|
"jump-to-post",
|
||||||
"login",
|
"login",
|
||||||
"move-to-topic",
|
"move-to-topic",
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
<DModalBody @title="composer.link_dialog_title" @class="insert-link">
|
|
||||||
<form id="insert-hyperlink-form" {{on "submit" (action "ok")}}>
|
|
||||||
<div class="inputs">
|
|
||||||
<TextField
|
|
||||||
@value={{this.linkUrl}}
|
|
||||||
@placeholderKey="composer.link_url_placeholder"
|
|
||||||
@class="link-url"
|
|
||||||
@key-up={{action "search"}}
|
|
||||||
@autofocus="autofocus"
|
|
||||||
/>
|
|
||||||
{{#if this.searchLoading}}
|
|
||||||
{{loading-spinner}}
|
|
||||||
{{/if}}
|
|
||||||
{{#if this.searchResults}}
|
|
||||||
<div class="internal-link-results">
|
|
||||||
{{#each this.searchResults as |result|}}
|
|
||||||
<a
|
|
||||||
class="search-link"
|
|
||||||
href={{result.url}}
|
|
||||||
onclick={{action "linkClick"}}
|
|
||||||
data-title={{result.fancy_title}}
|
|
||||||
>
|
|
||||||
<TopicStatus @topic={{result}} @disableActions={{true}} />
|
|
||||||
{{replace-emoji result.title}}
|
|
||||||
<div class="search-category">
|
|
||||||
{{#if result.category.parentCategory}}
|
|
||||||
{{category-link result.category.parentCategory}}
|
|
||||||
{{/if}}
|
|
||||||
{{category-link result.category hideParent=true}}
|
|
||||||
{{discourse-tags result}}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
<div class="inputs">
|
|
||||||
<TextField
|
|
||||||
@value={{this.linkText}}
|
|
||||||
@placeholderKey="composer.link_optional_text"
|
|
||||||
@class="link-text"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</DModalBody>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<DButton
|
|
||||||
@class="btn-primary"
|
|
||||||
@label="composer.modal_ok"
|
|
||||||
@action={{action "ok"}}
|
|
||||||
@type="submit"
|
|
||||||
@form="insert-hyperlink-form"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DButton
|
|
||||||
@class="btn-danger"
|
|
||||||
@label="composer.modal_cancel"
|
|
||||||
@action={{action "cancel"}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
|
@ -1,8 +1,4 @@
|
||||||
import {
|
import { acceptance, query } from "discourse/tests/helpers/qunit-helpers";
|
||||||
acceptance,
|
|
||||||
exists,
|
|
||||||
query,
|
|
||||||
} from "discourse/tests/helpers/qunit-helpers";
|
|
||||||
import { click, fillIn, triggerKeyEvent, visit } from "@ember/test-helpers";
|
import { click, fillIn, triggerKeyEvent, visit } from "@ember/test-helpers";
|
||||||
import { test } from "qunit";
|
import { test } from "qunit";
|
||||||
|
|
||||||
|
@ -14,28 +10,27 @@ acceptance("Composer - Hyperlink", function (needs) {
|
||||||
await click(".topic-post:first-child button.reply");
|
await click(".topic-post:first-child button.reply");
|
||||||
await fillIn(".d-editor-input", "This is a link to ");
|
await fillIn(".d-editor-input", "This is a link to ");
|
||||||
|
|
||||||
assert.ok(
|
assert
|
||||||
!exists(".insert-link.modal-body"),
|
.dom(".insert-link.modal-body")
|
||||||
"no hyperlink modal by default"
|
.doesNotExist("no hyperlink modal by default");
|
||||||
);
|
|
||||||
|
|
||||||
await click(".d-editor button.link");
|
await click(".d-editor button.link");
|
||||||
assert.ok(exists(".insert-link.modal-body"), "hyperlink modal visible");
|
assert.dom(".insert-link.modal-body").exists("hyperlink modal visible");
|
||||||
|
|
||||||
await fillIn(".modal-body .link-url", "google.com");
|
await fillIn(".modal-body .link-url", "google.com");
|
||||||
await fillIn(".modal-body .link-text", "Google");
|
await fillIn(".modal-body .link-text", "Google");
|
||||||
await click(".modal-footer button.btn-primary");
|
await click(".modal-footer button.btn-primary");
|
||||||
|
|
||||||
assert.strictEqual(
|
assert
|
||||||
query(".d-editor-input").value,
|
.dom(".d-editor-input")
|
||||||
"This is a link to [Google](https://google.com)",
|
.hasValue(
|
||||||
"adds link with url and text, prepends 'https://'"
|
"This is a link to [Google](https://google.com)",
|
||||||
);
|
"adds link with url and text, prepends 'https://'"
|
||||||
|
);
|
||||||
|
|
||||||
assert.ok(
|
assert
|
||||||
!exists(".insert-link.modal-body"),
|
.dom(".insert-link.modal-body")
|
||||||
"modal dismissed after submitting link"
|
.doesNotExist("modal dismissed after submitting link");
|
||||||
);
|
|
||||||
|
|
||||||
await fillIn(".d-editor-input", "Reset textarea contents.");
|
await fillIn(".d-editor-input", "Reset textarea contents.");
|
||||||
|
|
||||||
|
@ -44,16 +39,16 @@ acceptance("Composer - Hyperlink", function (needs) {
|
||||||
await fillIn(".modal-body .link-text", "Google");
|
await fillIn(".modal-body .link-text", "Google");
|
||||||
await click(".modal-footer button.btn-danger");
|
await click(".modal-footer button.btn-danger");
|
||||||
|
|
||||||
assert.strictEqual(
|
assert
|
||||||
query(".d-editor-input").value,
|
.dom(".d-editor-input")
|
||||||
"Reset textarea contents.",
|
.hasValue(
|
||||||
"doesn’t insert anything after cancelling"
|
"Reset textarea contents.",
|
||||||
);
|
"doesn’t insert anything after cancelling"
|
||||||
|
);
|
||||||
|
|
||||||
assert.ok(
|
assert
|
||||||
!exists(".insert-link.modal-body"),
|
.dom(".insert-link.modal-body")
|
||||||
"modal dismissed after cancelling"
|
.doesNotExist("modal dismissed after cancelling");
|
||||||
);
|
|
||||||
|
|
||||||
const textarea = query("#reply-control .d-editor-input");
|
const textarea = query("#reply-control .d-editor-input");
|
||||||
textarea.selectionStart = 0;
|
textarea.selectionStart = 0;
|
||||||
|
@ -63,48 +58,49 @@ acceptance("Composer - Hyperlink", function (needs) {
|
||||||
await fillIn(".modal-body .link-url", "somelink.com");
|
await fillIn(".modal-body .link-url", "somelink.com");
|
||||||
await click(".modal-footer button.btn-primary");
|
await click(".modal-footer button.btn-primary");
|
||||||
|
|
||||||
assert.strictEqual(
|
assert
|
||||||
query(".d-editor-input").value,
|
.dom(".d-editor-input")
|
||||||
"[Reset](https://somelink.com) textarea contents.",
|
.hasValue(
|
||||||
"adds link to a selected text"
|
"[Reset](https://somelink.com) textarea contents.",
|
||||||
);
|
"adds link to a selected text"
|
||||||
|
);
|
||||||
|
|
||||||
await fillIn(".d-editor-input", "");
|
await fillIn(".d-editor-input", "");
|
||||||
|
|
||||||
await click(".d-editor button.link");
|
await click(".d-editor button.link");
|
||||||
await fillIn(".modal-body .link-url", "http://google.com");
|
await fillIn(".modal-body .link-url", "http://google.com");
|
||||||
await triggerKeyEvent(".modal-body .link-url", "keyup", "Space");
|
await triggerKeyEvent(".modal-body .link-url", "keyup", "Space");
|
||||||
assert.ok(
|
assert
|
||||||
!exists(".internal-link-results"),
|
.dom(".internal-link-results")
|
||||||
"does not show internal links search dropdown when inputting a url"
|
.doesNotExist(
|
||||||
);
|
"does not show internal links search dropdown when inputting a url"
|
||||||
|
);
|
||||||
|
|
||||||
await fillIn(".modal-body .link-url", "local");
|
await fillIn(".modal-body .link-url", "local");
|
||||||
await triggerKeyEvent(".modal-body .link-url", "keyup", "Space");
|
await triggerKeyEvent(".modal-body .link-url", "keyup", "Space");
|
||||||
assert.ok(
|
assert
|
||||||
exists(".internal-link-results"),
|
.dom(".internal-link-results")
|
||||||
"shows internal links search dropdown when entering keywords"
|
.exists("shows internal links search dropdown when entering keywords");
|
||||||
);
|
|
||||||
|
|
||||||
await triggerKeyEvent(".insert-link", "keydown", "ArrowDown");
|
await triggerKeyEvent(".insert-link", "keydown", "ArrowDown");
|
||||||
await triggerKeyEvent(".insert-link", "keydown", "Enter");
|
await triggerKeyEvent(".insert-link", "keydown", "Enter");
|
||||||
|
|
||||||
assert.ok(
|
assert
|
||||||
!exists(".internal-link-results"),
|
.dom(".internal-link-results")
|
||||||
"search dropdown dismissed after selecting an internal link"
|
.doesNotExist(
|
||||||
);
|
"search dropdown dismissed after selecting an internal link"
|
||||||
|
);
|
||||||
|
|
||||||
assert.ok(
|
assert
|
||||||
query(".link-url").value.includes("http"),
|
.dom(".link-url")
|
||||||
"replaces link url field with internal link"
|
.hasValue(/http/, "replaces link url field with internal link");
|
||||||
);
|
|
||||||
|
|
||||||
await triggerKeyEvent(".insert-link", "keydown", "Escape");
|
await triggerKeyEvent(".insert-link", "keydown", "Escape");
|
||||||
|
|
||||||
assert.strictEqual(
|
assert
|
||||||
document.activeElement.classList.contains("d-editor-input"),
|
.dom(".d-editor-input")
|
||||||
true,
|
.isFocused(
|
||||||
"focus stays on composer after dismissing modal using Esc key"
|
"focus stays on composer after dismissing modal using Esc key"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -26,6 +26,7 @@ import {
|
||||||
renderUserStatusHtml,
|
renderUserStatusHtml,
|
||||||
} from "discourse/lib/user-status-on-autocomplete";
|
} from "discourse/lib/user-status-on-autocomplete";
|
||||||
import ChatModalChannelSummary from "discourse/plugins/chat/discourse/components/chat/modal/channel-summary";
|
import ChatModalChannelSummary from "discourse/plugins/chat/discourse/components/chat/modal/channel-summary";
|
||||||
|
import InsertHyperlink from "discourse/components/modal/insert-hyperlink";
|
||||||
|
|
||||||
export default class ChatComposer extends Component {
|
export default class ChatComposer extends Component {
|
||||||
@service capabilities;
|
@service capabilities;
|
||||||
|
@ -347,10 +348,12 @@ export default class ChatComposer extends Component {
|
||||||
|
|
||||||
const selected = this.composer.textarea.getSelected("", { lineVal: true });
|
const selected = this.composer.textarea.getSelected("", { lineVal: true });
|
||||||
const linkText = selected?.value;
|
const linkText = selected?.value;
|
||||||
showModal("insert-hyperlink").setProperties({
|
this.modal.show(InsertHyperlink, {
|
||||||
linkText,
|
model: {
|
||||||
toolbarEvent: {
|
linkText,
|
||||||
addText: (text) => this.composer.textarea.addText(selected, text),
|
toolbarEvent: {
|
||||||
|
addText: (text) => this.composer.textarea.addText(selected, text),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue