UX: Better composer hyperlink modal (#8160)
This commit is contained in:
parent
1ee633cea4
commit
30cda1761d
|
@ -21,6 +21,7 @@ import { wantsNewWindow } from "discourse/lib/intercept-click";
|
||||||
import { translations } from "pretty-text/emoji/data";
|
import { translations } from "pretty-text/emoji/data";
|
||||||
import { emojiSearch, isSkinTonableEmoji } from "pretty-text/emoji";
|
import { emojiSearch, isSkinTonableEmoji } from "pretty-text/emoji";
|
||||||
import { emojiUrlFor } from "discourse/lib/text";
|
import { emojiUrlFor } from "discourse/lib/text";
|
||||||
|
import showModal from "discourse/lib/show-modal";
|
||||||
|
|
||||||
// Our head can be a static string or a function that returns a string
|
// Our head can be a static string or a function that returns a string
|
||||||
// based on input (like for numbered lists).
|
// based on input (like for numbered lists).
|
||||||
|
@ -89,7 +90,7 @@ class Toolbar {
|
||||||
id: "link",
|
id: "link",
|
||||||
group: "insertions",
|
group: "insertions",
|
||||||
shortcut: "K",
|
shortcut: "K",
|
||||||
action: (...args) => this.context.send("showLinkModal", args)
|
sendAction: event => this.context.send("showLinkModal", event)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,9 +214,6 @@ export function onToolbarCreate(func) {
|
||||||
export default Ember.Component.extend({
|
export default Ember.Component.extend({
|
||||||
classNames: ["d-editor"],
|
classNames: ["d-editor"],
|
||||||
ready: false,
|
ready: false,
|
||||||
insertLinkHidden: true,
|
|
||||||
linkUrl: "",
|
|
||||||
linkText: "",
|
|
||||||
lastSel: null,
|
lastSel: null,
|
||||||
_mouseTrap: null,
|
_mouseTrap: null,
|
||||||
showLink: true,
|
showLink: true,
|
||||||
|
@ -946,21 +944,23 @@ export default Ember.Component.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
showLinkModal() {
|
showLinkModal(toolbarEvent) {
|
||||||
if (this.disabled) {
|
if (this.disabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.set("linkUrl", "");
|
let linkText = "";
|
||||||
this.set("linkText", "");
|
|
||||||
|
|
||||||
this._lastSel = this._getSelected();
|
this._lastSel = this._getSelected();
|
||||||
|
|
||||||
if (this._lastSel) {
|
if (this._lastSel) {
|
||||||
this.set("linkText", this._lastSel.value.trim());
|
linkText = this._lastSel.value.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.set("insertLinkHidden", false);
|
showModal("insert-hyperlink").setProperties({
|
||||||
|
linkText: linkText,
|
||||||
|
_lastSel: this._lastSel,
|
||||||
|
toolbarEvent
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
formatCode() {
|
formatCode() {
|
||||||
|
@ -1004,29 +1004,6 @@ export default Ember.Component.extend({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
insertLink() {
|
|
||||||
const origLink = this.linkUrl;
|
|
||||||
const linkUrl =
|
|
||||||
origLink.indexOf("://") === -1 ? `http://${origLink}` : origLink;
|
|
||||||
const sel = this._lastSel;
|
|
||||||
|
|
||||||
if (Ember.isEmpty(linkUrl)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const linkText = this.linkText || "";
|
|
||||||
if (linkText.length) {
|
|
||||||
this._addText(sel, `[${linkText}](${linkUrl})`);
|
|
||||||
} else {
|
|
||||||
if (sel.value) {
|
|
||||||
this._addText(sel, `[${sel.value}](${linkUrl})`);
|
|
||||||
} else {
|
|
||||||
this._addText(sel, `[${origLink}](${linkUrl})`);
|
|
||||||
this._selectText(sel.start + 1, origLink.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||||
|
|
||||||
|
export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
|
linkUrl: "",
|
||||||
|
linkText: "",
|
||||||
|
|
||||||
|
onShow() {
|
||||||
|
Ember.run.next(() =>
|
||||||
|
$(this)
|
||||||
|
.find("input.link-url")
|
||||||
|
.focus()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
ok() {
|
||||||
|
const origLink = this.linkUrl;
|
||||||
|
const linkUrl =
|
||||||
|
origLink.indexOf("://") === -1 ? `http://${origLink}` : origLink;
|
||||||
|
const sel = this._lastSel;
|
||||||
|
|
||||||
|
if (Ember.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.set("linkUrl", "");
|
||||||
|
this.set("linkText", "");
|
||||||
|
this.send("closeModal");
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
this.send("closeModal");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,13 +1,3 @@
|
||||||
<div class='d-editor-overlay hidden'></div>
|
|
||||||
|
|
||||||
<div class='d-editor-modals'>
|
|
||||||
{{#d-editor-modal class="insert-link" hidden=insertLinkHidden okAction=(action "insertLink")}}
|
|
||||||
<h3>{{i18n "composer.link_dialog_title"}}</h3>
|
|
||||||
{{text-field value=linkUrl placeholderKey="composer.link_url_placeholder" class="link-url"}}
|
|
||||||
{{text-field value=linkText placeholderKey="composer.link_optional_text" class="link-text"}}
|
|
||||||
{{/d-editor-modal}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='d-editor-container'>
|
<div class='d-editor-container'>
|
||||||
<div class="d-editor-textarea-wrapper {{if disabled "disabled"}}">
|
<div class="d-editor-textarea-wrapper {{if disabled "disabled"}}">
|
||||||
<div class='d-editor-button-bar'>
|
<div class='d-editor-button-bar'>
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
{{#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"}}
|
||||||
|
</div>
|
||||||
|
<div class="inputs">
|
||||||
|
{{text-field value=linkText placeholderKey="composer.link_optional_text" class="link-text"}}
|
||||||
|
</div>
|
||||||
|
{{/d-modal-body}}
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
{{d-button class="btn-primary" label="composer.modal_ok" action=(action "ok")}}
|
||||||
|
{{d-button class="btn-danger" label="composer.modal_cancel" action=(action "cancel")}}
|
||||||
|
</div>
|
|
@ -199,6 +199,12 @@
|
||||||
&.full-height-modal {
|
&.full-height-modal {
|
||||||
max-height: calc(100vh - 150px);
|
max-height: calc(100vh - 150px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.insert-link {
|
||||||
|
input {
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
textarea {
|
textarea {
|
||||||
width: 99%;
|
width: 99%;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
|
|
|
@ -4,43 +4,12 @@
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.d-editor-overlay {
|
|
||||||
position: absolute;
|
|
||||||
background-color: black;
|
|
||||||
opacity: 0.8;
|
|
||||||
z-index: z("modal", "overlay");
|
|
||||||
}
|
|
||||||
|
|
||||||
.d-editor-modals {
|
|
||||||
position: absolute;
|
|
||||||
z-index: z("modal", "content");
|
|
||||||
}
|
|
||||||
|
|
||||||
.d-editor {
|
.d-editor {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.d-editor .d-editor-modal {
|
|
||||||
min-width: 400px;
|
|
||||||
@media screen and (max-width: 424px) {
|
|
||||||
min-width: 300px;
|
|
||||||
}
|
|
||||||
position: absolute;
|
|
||||||
background-color: $secondary;
|
|
||||||
border: 1px solid $primary-low;
|
|
||||||
padding: 1em;
|
|
||||||
top: 25px;
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 95%;
|
|
||||||
}
|
|
||||||
h3 {
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.d-editor-textarea-wrapper,
|
.d-editor-textarea-wrapper,
|
||||||
.d-editor-preview-wrapper {
|
.d-editor-preview-wrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
|
@ -44,6 +44,12 @@
|
||||||
.category-chooser {
|
.category-chooser {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-body.insert-link {
|
||||||
|
input {
|
||||||
|
min-width: 450px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-category-modal {
|
.edit-category-modal {
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { acceptance } from "helpers/qunit-helpers";
|
||||||
|
|
||||||
|
acceptance("Composer - Hyperlink", {
|
||||||
|
loggedIn: true
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test("add a hyperlink to a reply", async assert => {
|
||||||
|
await visit("/t/internationalization-localization/280");
|
||||||
|
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,
|
||||||
|
"no hyperlink modal by default"
|
||||||
|
);
|
||||||
|
|
||||||
|
await click(".d-editor button.link");
|
||||||
|
assert.equal(
|
||||||
|
find(".insert-link.modal-body").length,
|
||||||
|
1,
|
||||||
|
"hyperlink modal visible"
|
||||||
|
);
|
||||||
|
|
||||||
|
await fillIn(".modal-body .link-url", "google.com");
|
||||||
|
await fillIn(".modal-body .link-text", "Google");
|
||||||
|
await click(".modal-footer button.btn-primary");
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
find(".d-editor-input").val(),
|
||||||
|
"This is a link to [Google](http://google.com)",
|
||||||
|
"adds link with url and text, prepends 'http://'"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
find(".insert-link.modal-body").length,
|
||||||
|
0,
|
||||||
|
"modal dismissed after submitting link"
|
||||||
|
);
|
||||||
|
|
||||||
|
await fillIn(".d-editor-input", "Reset textarea contents.");
|
||||||
|
|
||||||
|
await click(".d-editor button.link");
|
||||||
|
await fillIn(".modal-body .link-url", "google.com");
|
||||||
|
await fillIn(".modal-body .link-text", "Google");
|
||||||
|
await click(".modal-footer button.btn-danger");
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
find(".d-editor-input").val(),
|
||||||
|
"Reset textarea contents.",
|
||||||
|
"adds link with url and text, prepends 'http://'"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
find(".insert-link.modal-body").length,
|
||||||
|
0,
|
||||||
|
"modal dismissed after cancelling"
|
||||||
|
);
|
||||||
|
|
||||||
|
const textarea = find("#reply-control .d-editor-input")[0];
|
||||||
|
textarea.selectionStart = 0;
|
||||||
|
textarea.selectionEnd = 6;
|
||||||
|
await click(".d-editor button.link");
|
||||||
|
|
||||||
|
await fillIn(".modal-body .link-url", "somelink.com");
|
||||||
|
await click(".modal-footer button.btn-primary");
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
find(".d-editor-input").val(),
|
||||||
|
"[Reset](http://somelink.com) textarea contents.",
|
||||||
|
"adds link to a selected text"
|
||||||
|
);
|
||||||
|
});
|
|
@ -200,62 +200,6 @@ testCase(`italic with a multiline selection`, async function(assert, textarea) {
|
||||||
assert.equal(textarea.selectionEnd, 12);
|
assert.equal(textarea.selectionEnd, 12);
|
||||||
});
|
});
|
||||||
|
|
||||||
testCase("link modal (cancel)", async function(assert) {
|
|
||||||
assert.equal(find(".insert-link.hidden").length, 1);
|
|
||||||
|
|
||||||
await click("button.link");
|
|
||||||
assert.equal(find(".insert-link.hidden").length, 0);
|
|
||||||
|
|
||||||
await click(".insert-link button.btn-danger");
|
|
||||||
assert.equal(find(".insert-link.hidden").length, 1);
|
|
||||||
assert.equal(this.value, "hello world.");
|
|
||||||
});
|
|
||||||
|
|
||||||
testCase("link modal (simple link)", async function(assert, textarea) {
|
|
||||||
await click("button.link");
|
|
||||||
|
|
||||||
const url = "http://eviltrout.com";
|
|
||||||
|
|
||||||
await fillIn(".insert-link input.link-url", url);
|
|
||||||
await click(".insert-link button.btn-primary");
|
|
||||||
assert.equal(find(".insert-link.hidden").length, 1);
|
|
||||||
assert.equal(this.value, `hello world.[${url}](${url})`);
|
|
||||||
assert.equal(textarea.selectionStart, 13);
|
|
||||||
assert.equal(textarea.selectionEnd, 13 + url.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
testCase("link modal auto http addition", async function(assert) {
|
|
||||||
await click("button.link");
|
|
||||||
await fillIn(".insert-link input.link-url", "sam.com");
|
|
||||||
await click(".insert-link button.btn-primary");
|
|
||||||
assert.equal(this.value, `hello world.[sam.com](http://sam.com)`);
|
|
||||||
});
|
|
||||||
|
|
||||||
testCase("link modal (simple link) with selected text", async function(
|
|
||||||
assert,
|
|
||||||
textarea
|
|
||||||
) {
|
|
||||||
textarea.selectionStart = 0;
|
|
||||||
textarea.selectionEnd = 12;
|
|
||||||
|
|
||||||
await click("button.link");
|
|
||||||
assert.equal(find("input.link-text")[0].value, "hello world.");
|
|
||||||
|
|
||||||
await fillIn(".insert-link input.link-url", "http://eviltrout.com");
|
|
||||||
await click(".insert-link button.btn-primary");
|
|
||||||
assert.equal(find(".insert-link.hidden").length, 1);
|
|
||||||
assert.equal(this.value, "[hello world.](http://eviltrout.com)");
|
|
||||||
});
|
|
||||||
|
|
||||||
testCase("link modal (link with description)", async function(assert) {
|
|
||||||
await click("button.link");
|
|
||||||
await fillIn(".insert-link input.link-url", "http://eviltrout.com");
|
|
||||||
await fillIn(".insert-link input.link-text", "evil trout");
|
|
||||||
await click(".insert-link button.btn-primary");
|
|
||||||
assert.equal(find(".insert-link.hidden").length, 1);
|
|
||||||
assert.equal(this.value, "hello world.[evil trout](http://eviltrout.com)");
|
|
||||||
});
|
|
||||||
|
|
||||||
componentTest("advanced code", {
|
componentTest("advanced code", {
|
||||||
template: "{{d-editor value=value}}",
|
template: "{{d-editor value=value}}",
|
||||||
beforeEach() {
|
beforeEach() {
|
||||||
|
|
Loading…
Reference in New Issue